v0.247.0: i18n spike — the dashboard can speak English, Hungarian byte-identical
gates / gates (push) Successful in 19s

Message bundles (internal/i18n) expanded into templates before parsing, one
template set per language. Launcher, /backups, /apps/<slug> and the layout
converted; household language setting, POST /settings/language, ?lang= override,
report field. Parity test against fixtures captured from unconverted templates;
copy gates read templates expanded; new i18n_missing_gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-17 14:50:57 +02:00
parent 89dd3e94b1
commit 612c417024
49 changed files with 11734 additions and 248 deletions
+48 -49
View File
@@ -3,27 +3,27 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<a href="/stacks" class="btn btn-sm btn-outline">{{T "app_info.alkalmazasok"}}</a>
<h2>{{.Meta.DisplayName}}</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
{{if .Stack.Deployed}}
<span class="stack-state-badge state-{{stateColor .Stack.State}}">{{stateLabel .Stack.State}}</span>
{{if .Stack.Orphaned}}<span class="badge badge-orphaned">Elavult</span>{{end}}
{{if .Stack.Orphaned}}<span class="badge badge-orphaned">{{T "app_info.elavult"}}</span>{{end}}
{{template "meta_badge" (lifecycleBadge .Meta)}}
{{template "meta_badge" (updateBadge .Stack)}}
{{if .EffectiveSubdomain}}<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>{{end}}
<a href="/stacks/{{.Stack.Name}}/logs" class="btn btn-sm btn-outline">Napló</a>
{{if .EffectiveSubdomain}}<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline">{{T "app_info.megnyitas"}}</a>{{end}}
<a href="/stacks/{{.Stack.Name}}/logs" class="btn btn-sm btn-outline">{{T "app_info.naplo"}}</a>
{{if .Stack.Orphaned}}
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Stack.Name}}')">Törlés</button>
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Stack.Name}}')">{{T "app_info.torles"}}</button>
{{else}}
<a href="/stacks/{{.Stack.Name}}/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-outline">Beállítások</a>
<a href="/stacks/{{.Stack.Name}}/export" class="btn btn-sm btn-outline">{{T "app_info.exportalas"}}</a>
<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-outline">{{T "app_info.beallitasok"}}</a>
{{end}}
{{else}}
{{/* Server-side the deploy endpoint refuses a non-installable template; hide the button
so the page never offers an action that would be rejected. */}}
{{if canInstall .Meta}}<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">Telepítés</a>{{end}}
{{if canInstall .Meta}}<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">{{T "app_info.telepites"}}</a>{{end}}
{{end}}
</div>
</div>
@@ -31,15 +31,15 @@
{{if .Stack.Updating}}
<div class="alert alert-info" style="margin-top:1rem" data-update-phase="{{.Stack.UpdatePhase}}">{{.Stack.UpdatePhaseLabel}}</div>
{{else if .Stack.HoldReason}}
<div class="alert alert-error" style="margin-top:1rem" data-held="true">{{.Stack.HoldReason}} <a href="/backups/apps" class="btn btn-sm btn-outline">Mentések</a></div>
<div class="alert alert-error" style="margin-top:1rem" data-held="true">{{.Stack.HoldReason}} <a href="/backups/apps" class="btn btn-sm btn-outline">{{T "app_info.mentesek"}}</a></div>
{{else if .Stack.UpdateError}}
<div class="alert alert-warning" style="margin-top:1rem" data-update-error="true">{{.Stack.UpdateError}}</div>
{{end}}
{{if .MissingStorageLabel}}
<div class="alert alert-warning" style="margin-top:1rem">
<strong><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Hiányzó tárhely: {{.MissingStorageLabel}}</strong><br>
Ennek az alkalmazásnak az adattárolója jelenleg nem elérhető, ezért le van állítva. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre.
<strong><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "app_info.hianyzo_tarhely"}} {{.MissingStorageLabel}}</strong><br>
{{T "app_info.ennek_az_alkalmazasnak_az_adattaroloja"}}
</div>
{{end}}
@@ -57,14 +57,13 @@
<div class="stack-meta-badges">
<span class="meta-badge">~{{.Meta.Resources.MemRequest}} RAM</span>
<span class="meta-badge">{{.Meta.Category}}</span>
{{if .Meta.Resources.NeedsHDD}}<span class="meta-badge meta-badge-warn">HDD szükséges</span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="meta-badge meta-badge-ok">Pi kompatibilis</span>{{else}}<span class="meta-badge meta-badge-warn">Csak x86</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="meta-badge meta-badge-ok">Magyar felület</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="meta-badge meta-badge-warn">{{T "app_info.hdd_szukseges"}}</span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="meta-badge meta-badge-ok">{{T "app_info.pi_kompatibilis"}}</span>{{else}}<span class="meta-badge meta-badge-warn">{{T "app_info.csak_x86"}}</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="meta-badge meta-badge-ok">{{T "app_info.magyar_felulet"}}</span>{{end}}
</div>
{{if .Meta.IsAbandoned}}
<div class="alert alert-warning" style="margin-top:.75rem">
Az alkalmazás fejlesztője felhagyott a fejlesztéssel. A telepített verzió továbbra is használható,
de frissítések és biztonsági javítások már nem érkeznek hozzá.
{{T "app_info.az_alkalmazas_fejlesztoje_felhagyott_a"}}
</div>
{{end}}
</div>
@@ -82,8 +81,8 @@
{{if .DataPathCards}}
<div class="app-info-card" style="margin-top:1rem">
<h3>Hova tegyem a fájlokat?</h3>
<p class="form-hint">Ezeket a mappákat a Fájlkezelőben éred el. Előfordulhat, hogy először be kell jelentkezned a Fájlkezelőbe.</p>
<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">
@@ -100,21 +99,21 @@
{{if and .Stack.Deployed .MigrateTargets}}
<div class="app-info-card" style="margin-top:1rem">
<h3>Áthelyezés másik tárhelyre</h3>
<p class="form-hint">Ennek az alkalmazásnak az adatait másik csatlakoztatott tárhelyre helyezheted át. Az alkalmazás az áthelyezés alatt rövid időre leáll, az adatok pedig csak az ellenőrzés és a sikeres újraindítás után törlődnek a régi helyről.</p>
<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="">Válassz céltárhelyet…</option>
<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}}')">Áthelyezés</button>
<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 (Number(b||0)/1e9).toFixed(1)+' GB'; }
function appMigRender(job){
var names={stop:'Leállítás',copy:'Adatok másolása',verify:'Ellenőrzés',flip:'Újratelepítés',redeploy:'Újratelepítés',cleanup:'Régi adatok törlése'};
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+'…';
@@ -125,10 +124,10 @@ function appMigWatch(){
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='Nincs folyamatban áthelyezés.'; return; }
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>Kész</strong>'; setTimeout(function(){location.reload();},1500); return; }
if(job.phase==='aborted'){ if(panel) panel.innerHTML+='<br><strong style="color:var(--danger,#c0392b)">Megszakadt: '+(job.error||'')+'</strong><br>A régi adatok érintetlenek.'; return; }
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); });
}
@@ -137,11 +136,11 @@ function appMigWatch(){
function appMigrate(btn,app,label){
var sel=document.getElementById('app-migrate-target');
var target=sel?sel.value:'';
if(!target){ showAlert('Válassz céltárhelyet.'); return; }
felhomConfirm(btn,'Áthelyezed a(z) '+label+' adatait ide: '+target+'? Az alkalmazás rövid időre leáll. A régi adatok csak sikeres áthelyezés után törlődnek.',function(){
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('Hiba: '+(d.error||'ismeretlen')); } })
.catch(function(e){ showAlert('Hiba: '+e); });
.then(function(r){return r.json();}).then(function(d){ if(d.ok){ appMigWatch(); } else { showAlert('{{T "app_info.hiba"}} '+(d.error||'{{T "app_info.ismeretlen"}}')); } })
.catch(function(e){ showAlert('{{T "app_info.hiba"}} '+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(){}); })();
@@ -152,7 +151,7 @@ function appMigrate(btn,app,label){
<div class="app-info-grid">
{{if .AppInfo.UseCases}}
<div class="app-info-card">
<h3>Mire használható?</h3>
<h3>{{T "app_info.mire_hasznalhato"}}</h3>
<ul class="app-info-list">
{{range .AppInfo.UseCases}}<li>{{.}}</li>{{end}}
</ul>
@@ -161,7 +160,7 @@ function appMigrate(btn,app,label){
{{if .AppInfo.FirstSteps}}
<div class="app-info-card">
<h3>Első lépések</h3>
<h3>{{T "app_info.elso_lepesek"}}</h3>
<ol class="app-info-list">
{{range .AppInfo.FirstSteps}}<li>{{.}}</li>{{end}}
</ol>
@@ -170,7 +169,7 @@ function appMigrate(btn,app,label){
{{if .AppInfo.Prerequisites}}
<div class="app-info-card">
<h3>Előfeltételek</h3>
<h3>{{T "app_info.elofeltetelek"}}</h3>
<ul class="app-info-list">
{{range .AppInfo.Prerequisites}}<li>{{.}}</li>{{end}}
</ul>
@@ -179,51 +178,51 @@ function appMigrate(btn,app,label){
{{if .InitialCreds}}
<div class="app-info-card">
<h3>Kezdeti belépési adatok</h3>
<h3>{{T "app_info.kezdeti_belepesi_adatok"}}</h3>
<table class="initcred-table">
{{if .InitialCreds.Username}}
<tr>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">Felhasználónév</td>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "app_info.felhasznalonev"}}</td>
<td><code class="initcred-user" style="user-select:all">{{.InitialCreds.Username}}</code></td>
</tr>
{{end}}
{{if .InitialCredsOperatorSet}}
<tr>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">Jelszó</td>
<td>az üzemeltető állította be</td>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "app_info.jelszo"}}</td>
<td>{{T "app_info.az_uzemelteto_allitotta_be"}}</td>
</tr>
{{else}}
<tr>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap;vertical-align:middle">Jelszó</td>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap;vertical-align:middle">{{T "app_info.jelszo"}}</td>
<!-- R-254: the value is NOT in this page. It used to be rendered into a `hidden`
span, which stops a browser drawing it and nothing else — a fetch of this page
returned a real per-install password. Both buttons now ask the server. -->
<td style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
<code id="initcred-pw">••••••••••••</code>
<button type="button" class="btn btn-sm btn-outline" id="initcred-reveal" onclick="icRevealPw(this)">Megjelenítés</button>
<button type="button" class="btn btn-sm btn-outline" onclick="icCopyPw(this)">Másolás</button>
<button type="button" class="btn btn-sm btn-outline" id="initcred-reveal" onclick="icRevealPw(this)">{{T "app_info.megjelenites"}}</button>
<button type="button" class="btn btn-sm btn-outline" onclick="icCopyPw(this)">{{T "app_info.masolas"}}</button>
<span id="initcred-err" class="form-hint" style="display:none;color:var(--red)"></span>
</td>
</tr>
{{end}}
</table>
{{if .InitialCreds.Note}}<p class="app-info-creds-warn">{{.InitialCreds.Note}}</p>{{end}}
{{if not .InitialCredsOperatorSet}}<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg! Ez a kezdeti, automatikusan generált jelszó — ha már megváltoztattad, hagyd figyelmen kívül.</p>{{end}}
{{if not .InitialCredsOperatorSet}}<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal"}}</p>{{end}}
</div>
{{end}}
{{if .AppInfo.DefaultCreds}}
<div class="app-info-card">
<h3>Alapértelmezett belépés</h3>
<h3>{{T "app_info.alapertelmezett_belepes"}}</h3>
<p class="app-info-creds">{{.AppInfo.DefaultCreds}}</p>
<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg!</p>
<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal_2"}}</p>
</div>
{{end}}
{{if .AppInfo.DocsURL}}
<div class="app-info-card">
<h3>Dokumentáció</h3>
<p><a href="{{.AppInfo.DocsURL}}" target="_blank" class="app-info-link">Hivatalos dokumentáció ↗</a></p>
<h3>{{T "app_info.dokumentacio"}}</h3>
<p><a href="{{.AppInfo.DocsURL}}" target="_blank" class="app-info-link">{{T "app_info.hivatalos_dokumentacio"}}</a></p>
</div>
{{end}}
</div>
@@ -240,7 +239,7 @@ function icFetchPw() {
headers: {'X-CSRF-Token': '{{.CSRFToken}}'},
credentials: 'same-origin'
}).then(function (r) { return r.json(); }).then(function (j) {
if (!j.ok) { throw new Error(j.error || 'A kezdeti jelszó beolvasása nem sikerült.'); }
if (!j.ok) { throw new Error(j.error || '{{T "app_info.a_kezdeti_jelszo_beolvasasa_nem"}}'); }
return j.data.password;
});
}
@@ -256,7 +255,7 @@ function icRevealPw(btn) {
if (code.dataset.shown === '1') { // hide: drop the value out of the DOM again
code.textContent = '••••••••••••';
code.dataset.shown = '0';
btn.textContent = 'Megjelenítés';
btn.textContent = '{{T "app_info.megjelenites"}}';
return;
}
btn.disabled = true;
@@ -264,7 +263,7 @@ function icRevealPw(btn) {
btn.disabled = false;
code.textContent = pw;
code.dataset.shown = '1';
btn.textContent = 'Elrejtés';
btn.textContent = '{{T "app_info.elrejtes"}}';
}).catch(function (e) { btn.disabled = false; icErr(e.message); });
}
function icCopyPw(btn) {
@@ -274,7 +273,7 @@ function icCopyPw(btn) {
return navigator.clipboard.writeText(pw).then(function () {
btn.disabled = false;
var orig = btn.textContent;
btn.textContent = 'Másolva';
btn.textContent = '{{T "app_info.masolva"}}';
setTimeout(function () { btn.textContent = orig; }, 1500);
});
}).catch(function (e) { btn.disabled = false; icErr(e.message); });
+52 -52
View File
@@ -2,7 +2,7 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Biztonsági mentés</h2>
<h2>{{T "backups.biztonsagi_mentes"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
@@ -20,7 +20,7 @@
<p class="form-hint">{{.BackupTarget.OfferLabel}}</p>
<button type="button" class="btn btn-sm" id="backup-target-assign"
data-path="{{.BackupTarget.OfferPath}}"
data-confirm="Kijelölöd ezt a meghajtót a rendszermentés helyéül?">Kijelölöm</button>
data-confirm="{{T "backups.kijelolod_ezt_a_meghajtot_a"}}">{{T "backups.kijelolom"}}</button>
<div id="backup-target-assign-result"></div>
</div>
{{end}}
@@ -32,13 +32,13 @@
<!-- Section 0: Storage overview -->
<div class="backup-section-card">
<h3>Tárhely áttekintés</h3>
<h3>{{T "backups.tarhely_attekintes"}}</h3>
<div class="storage-overview-grid">
<div class="storage-bars">
{{with $.SystemInfo}}
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">Rendszer (/)</span>
<span class="storage-label">{{T "backups.rendszer"}}</span>
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
</div>
<div class="meter-track">
@@ -50,7 +50,7 @@
<div class="storage-item meter meter-empty storage-disconnected">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value">— <span class="state-text-neutral">Leválasztva</span></span>
<span class="storage-value">— <span class="state-text-neutral">{{T "backups.levalasztva"}}</span></span>
</div>
<div class="meter-track"></div>
</div>
@@ -70,8 +70,8 @@
</div>
<div class="storage-stats">
<div class="storage-stat-row">
<span class="storage-stat-label">DB mentések</span>
<span class="storage-stat-value mono">{{if .Backup.DumpFiles}}{{len .Backup.DumpFiles}} fájl{{else}}–{{end}}</span>
<span class="storage-stat-label">{{T "backups.db_mentesek"}}</span>
<span class="storage-stat-value mono">{{if .Backup.DumpFiles}}{{T "backups.fajl"}}{{else}}–{{end}}</span>
</div>
</div>
</div>
@@ -80,32 +80,32 @@
<!-- Section: Whole-guest (appliance) backup — agent-sourced, read-only + manual trigger -->
{{with .GuestBackup}}
<div class="backup-section-card">
<h3>Rendszermentés (teljes mentés)</h3>
<p class="form-hint" style="margin-bottom:1rem">A teljes szerver — alkalmazások, beállítások és adatbázisok együtt — időszakos mentése, amelyből az egész készülék visszaállítható. Ezt a host-ügynök készíti és kezeli.</p>
<h3>{{T "backups.rendszermentes_teljes_mentes"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">{{T "backups.a_teljes_szerver_alkalmazasok_beallitaso"}}</p>
{{if not .Available}}
<div class="alert alert-info">{{.Note}}</div>
{{else}}
<div class="stats-grid backup-page-cards">
<div class="stat-card {{if .HasBackup}}{{if .Success}}stat-running{{else}}stat-stopped{{end}}{{end}}">
<div class="stat-value">{{if .HasBackup}}{{if .Success}}&#10003;{{else}}&#10007;{{end}}{{else}}–{{end}}</div>
<div class="stat-label">Utolsó teljes mentés
<div class="stat-label">{{T "backups.utolso_teljes_mentes"}}
{{if .HasBackup}}<br><span class="relative-time">{{fmtTime .StartedAt}} ({{timeAgo .StartedAt}})</span>{{end}}
</div>
</div>
<div class="stat-card stat-total">
<div class="stat-value">{{if and .HasBackup (not .SizeUnknown)}}{{fmtBytes .SizeBytes}}{{else}}–{{end}}</div>
<div class="stat-label">{{if .HasBackup}}{{.Target}}{{else}}Méret / cél{{end}}</div>
<div class="stat-label">{{if .HasBackup}}{{.Target}}{{else}}{{T "backups.meret_cel"}}{{end}}</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.15rem">{{if .Due}}Esedékes{{else}}Naprakész{{end}}</div>
<div class="stat-label">Következő mentés
{{if not .Due}}<br><span class="relative-time">{{.AgeHours}} órája — a mentési ablakon belül</span>{{end}}
<div class="stat-value" style="font-size:1.15rem">{{if .Due}}{{T "backups.esedekes"}}{{else}}{{T "backups.naprakesz"}}{{end}}</div>
<div class="stat-label">{{T "backups.kovetkezo_mentes"}}
{{if not .Due}}<br><span class="relative-time">{{T "backups.oraja_a_mentesi_ablakon_belul"}}</span>{{end}}
</div>
</div>
<div class="stat-card">
<div class="stat-value">{{if .HasRestoreTest}}{{if .RestorePass}}&#10003;{{else}}&#10007;{{end}}{{else}}–{{end}}</div>
<div class="stat-label">Visszaállítás ellenőrizve
{{if .HasRestoreTest}}<br><span class="relative-time">{{fmtTime .RestoreTestedAt}}</span>{{else}}<br><span class="relative-time">Még nem futott</span>{{end}}
<div class="stat-label">{{T "backups.visszaallitas_ellenorizve"}}
{{if .HasRestoreTest}}<br><span class="relative-time">{{fmtTime .RestoreTestedAt}}</span>{{else}}<br><span class="relative-time">{{T "backups.meg_nem_futott"}}</span>{{end}}
</div>
</div>
</div>
@@ -116,16 +116,16 @@
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>{{.Label}}</strong></td>
<td style="padding:.5rem 0;vertical-align:top">
{{if .NotSetUp}}
<span class="tag"><span class="dot"></span>nincs beállítva</span>
<span class="tag"><span class="dot"></span>{{T "backups.nincs_beallitva"}}</span>
{{else}}
{{if .HasSuccess}}
<div>&#10003; Utolsó sikeres mentés: {{fmtTime .SuccessAt}} ({{timeAgo .SuccessAt}}){{if .SizeKnown}} · {{fmtBytes .SizeBytes}}{{end}}
{{if .Current}}<span class="tag tag-run" style="margin-left:.4rem">Naprakész</span>{{else}}<span class="tag tag-warn" style="margin-left:.4rem">Esedékes</span>{{end}}</div>
<div>{{T "backups.utolso_sikeres_mentes"}}{{if .SizeKnown}} · {{fmtBytes .SizeBytes}}{{end}}
{{if .Current}}<span class="tag tag-run" style="margin-left:.4rem">{{T "backups.naprakesz"}}</span>{{else}}<span class="tag tag-warn" style="margin-left:.4rem">{{T "backups.esedekes"}}</span>{{end}}</div>
{{else}}
<div>– Még nincs sikeres mentés <span class="tag tag-warn" style="margin-left:.4rem">Esedékes</span></div>
<div>{{T "backups.meg_nincs_sikeres_mentes"}} <span class="tag tag-warn" style="margin-left:.4rem">{{T "backups.esedekes"}}</span></div>
{{end}}
{{if .FailedAfter}}
<div style="color:var(--red);margin-top:.25rem">&#10007; {{fmtTime .FailedAt}} — sikertelen</div>
<div style="color:var(--red);margin-top:.25rem">{{T "backups.sikertelen"}}</div>
{{end}}
{{end}}
</td>
@@ -134,12 +134,12 @@
</table>
{{end}}
{{if .Running}}
<div class="alert alert-info" id="wg-running">Mentés folyamatban… (fázis: <span id="wg-phase">{{.Phase}}</span>)</div>
<div class="alert alert-info" id="wg-running">{{T "backups.mentes_folyamatban_fazis"}} <span id="wg-phase">{{.Phase}}</span>)</div>
{{end}}
{{if .CanTrigger}}
<div class="schedule-actions" style="margin-top:1rem">
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" {{if .Running}}disabled{{end}}>Mentés most</button>
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.</span>
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" {{if .Running}}disabled{{end}}>{{T "backups.mentes_most"}}</button>
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "backups.a_mentes_alatt_az_alkalmazasok"}}</span>
<div id="wg-backup-result" style="margin-top:.6rem"></div>
</div>
{{end}}
@@ -149,21 +149,21 @@
<!-- Section: customer-configurable backup window (v0.168.0) — one setting drives every nightly leg -->
<div class="backup-section-card">
<h3>Mentési időablak</h3>
<p class="form-hint" style="margin-bottom:1rem">A mentések egymás után futnak: adatbázis-mentés, helyi másolat, távoli mentés, majd a teljes rendszermentés.</p>
<h3>{{T "backups.mentesi_idoablak"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">{{T "backups.a_mentesek_egymas_utan_futnak"}}</p>
<form method="POST" action="/backups/window" class="schedule-actions" style="display:flex;align-items:flex-end;gap:.75rem;flex-wrap:wrap">
{{.CSRFField}}
<div class="form-group" style="margin:0">
<label for="window_start">Mentési időablak kezdete</label>
<label for="window_start">{{T "backups.mentesi_idoablak_kezdete"}}</label>
<input type="time" id="window_start" name="window_start" value="{{.BackupWindow}}" class="form-control" style="max-width:9rem" required>
</div>
<button type="submit" class="btn btn-sm btn-primary">Mentés</button>
<button type="submit" class="btn btn-sm btn-primary">{{T "backups.mentes"}}</button>
</form>
<ul class="form-hint" style="margin-top:1rem;margin-bottom:0;list-style:none;padding:0;line-height:1.9">
<li>Adatbázis-mentés: <strong>{{.BackupLegDB}}</strong></li>
<li>Helyi másolat: <strong>{{.BackupLegTier2}}</strong></li>
<li>Távoli mentés: <strong>{{.BackupLegOffbox}}</strong></li>
<li>Teljes rendszermentés: kb. <strong>{{.BackupGateFrom}}–{{.BackupGateTo}}</strong> között</li>
<li>{{T "backups.adatbazis_mentes"}}</li>
<li>{{T "backups.helyi_masolat"}}</li>
<li>{{T "backups.tavoli_mentes"}}</li>
<li>{{T "backups.teljes_rendszermentes_kb_kozott"}}</li>
</ul>
</div>
@@ -173,42 +173,42 @@
{{if .Backup.LastDBDump.Success}}
<div class="stat-card stat-running">
<div class="stat-value">&#10003;</div>
<div class="stat-label">Adatmentés aktív</div>
<div class="stat-label">{{T "backups.adatmentes_aktiv"}}</div>
</div>
{{else}}
<div class="stat-card stat-stopped">
<div class="stat-value">&#10007;</div>
<div class="stat-label">Adatmentés sikertelen</div>
<div class="stat-label">{{T "backups.adatmentes_sikertelen"}}</div>
</div>
{{end}}
{{else}}
<div class="stat-card">
<div class="stat-value">–</div>
<div class="stat-label">Adatmentés</div>
<div class="stat-label">{{T "backups.adatmentes"}}</div>
</div>
{{end}}
{{if and .GuestBackup .GuestBackup.Offsite}}
<div class="stat-card stat-running">
<div class="stat-value">&#10003;</div>
<div class="stat-label">Távoli rendszermentés<br><span class="relative-time">külön hardveren (PBS)</span></div>
<div class="stat-label">{{T "backups.tavoli_rendszermentes"}}<br><span class="relative-time">{{T "backups.kulon_hardveren_pbs"}}</span></div>
</div>
{{else}}
<div class="stat-card" style="border-left-color: var(--text-3);">
<div class="stat-value" style="background:var(--text-3);-webkit-background-clip:text;background-clip:text;">–</div>
<div class="stat-label">Távoli rendszermentés<br><span class="relative-time">nincs beállítva</span></div>
<div class="stat-label">{{T "backups.tavoli_rendszermentes"}}<br><span class="relative-time">{{T "backups.nincs_beallitva"}}</span></div>
</div>
{{end}}
<div class="stat-card stat-total">
{{if eq .DBSectionState "embedded"}}
<div class="stat-value">–</div>
<div class="stat-label">Adatbázis mentve<br><span class="relative-time">beágyazott DB-k a kötetmentésben</span></div>
<div class="stat-label">{{T "backups.adatbazis_mentve"}}<br><span class="relative-time">{{T "backups.beagyazott_db_k_a_kotetmentesben"}}</span></div>
{{else}}
<div class="stat-value">
{{if .Backup.LastDBDump}}{{len .Backup.LastDBDump.Results}}{{else}}{{len .Backup.DumpFiles}}{{end}}
</div>
<div class="stat-label">Adatbázis mentve</div>
<div class="stat-label">{{T "backups.adatbazis_mentve"}}</div>
{{end}}
</div>
</div>
@@ -220,18 +220,18 @@
// stop stacks → agent vzdump → resume). Returns immediately; we poll /api/guest-backup/status.
function triggerGuestBackup() {
var btn = document.getElementById('wg-backup-btn');
felhomConfirm(btn, 'Elindítod a teljes rendszermentést most? A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.', function () {
felhomConfirm(btn, '{{T "backups.elinditod_a_teljes_rendszermentest_most"}}', function () {
var out = document.getElementById('wg-backup-result');
btn.disabled = true;
out.innerHTML = '<span class="form-hint">Mentés indítása…</span>';
out.innerHTML = '<span class="form-hint">{{T "backups.mentes_inditasa"}}</span>';
fetch('/api/guest-backup/trigger', { method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()) })
.then(r => r.json())
.then(data => {
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || 'Hiba') + '</div>'; btn.disabled = false; return; }
out.innerHTML = '<span class="form-hint">Mentés folyamatban…</span>';
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || '{{T "backups.hiba"}}') + '</div>'; btn.disabled = false; return; }
out.innerHTML = '<span class="form-hint">{{T "backups.mentes_folyamatban"}}</span>';
pollGuestBackup(out, btn);
})
.catch(e => { out.innerHTML = '<div class="alert alert-error">Hiba: ' + e + '</div>'; btn.disabled = false; });
.catch(e => { out.innerHTML = '<div class="alert alert-error">{{T "backups.hiba_2"}} ' + e + '</div>'; btn.disabled = false; });
});
}
@@ -244,9 +244,9 @@ function pollGuestBackup(out, btn) {
.then(data => {
if (data.ok && data.data) {
var ph = data.data.phase;
out.innerHTML = '<span class="form-hint">Állapot: ' + ph + '</span>';
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">A rendszermentés elkészült.</div>'; setTimeout(() => window.location.reload(), 1500); }
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">A mentés sikertelen.</div>'; btn.disabled = false; }
out.innerHTML = '<span class="form-hint">{{T "backups.allapot"}} ' + ph + '</span>';
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">{{T "backups.a_rendszermentes_elkeszult"}}</div>'; setTimeout(() => window.location.reload(), 1500); }
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">{{T "backups.a_mentes_sikertelen"}}</div>'; btn.disabled = false; }
}
})
.catch(() => {});
@@ -263,7 +263,7 @@ function pollGuestBackup(out, btn) {
btn.addEventListener('click', function () {
var out = document.getElementById('backup-target-assign-result');
btn.disabled = true;
out.innerHTML = '<p class="form-hint">Kijelölés folyamatban…</p>';
out.innerHTML = '<p class="form-hint">{{T "backups.kijeloles_folyamatban"}}</p>';
fetch('/api/storage/backup-target/assign', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()),
@@ -272,20 +272,20 @@ function pollGuestBackup(out, btn) {
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.ok) {
out.innerHTML = '<div class="alert alert-error">A kijelölés nem sikerült: ' + (j.error || '') + '</div>';
out.innerHTML = '<div class="alert alert-error">{{T "backups.a_kijeloles_nem_sikerult"}} ' + (j.error || '') + '</div>';
btn.disabled = false;
return;
}
// The agent deliberately does NOT restart itself — restarting with a backup in flight
// records a spurious tier failure. So say so plainly instead of hiding it or faking it.
if (j.data && j.data.restart_required) {
out.innerHTML = '<div class="flash flash-success">A meghajtó kijelölve. A beállítás a host-ügynök következő újraindulása után lép életbe.</div>';
out.innerHTML = '<div class="flash flash-success">{{T "backups.a_meghajto_kijelolve_a_beallitas"}}</div>';
} else {
out.innerHTML = '<div class="flash flash-success">A meghajtó kijelölve.</div>';
out.innerHTML = '<div class="flash flash-success">{{T "backups.a_meghajto_kijelolve"}}</div>';
}
})
.catch(function (e) {
out.innerHTML = '<div class="alert alert-error">Hiba: ' + e.message + '</div>';
out.innerHTML = '<div class="alert alert-error">{{T "backups.hiba_2"}} ' + e.message + '</div>';
btn.disabled = false;
});
});
@@ -10,9 +10,8 @@
{{define "backups_empty"}}
<div class="backup-empty-state">
<div class="backup-empty-icon">&#128737;</div>
<h3>Biztonsági mentés nincs beállítva</h3>
<p>A biztonsági mentés funkció nem aktív.<br>
Kérjük, vegye fel a kapcsolatot a Felhom csapattal a beállításhoz.</p>
<h3>{{T "backups_shared.empty_title"}}</h3>
<p>{{T "backups_shared.empty_body"}}</p>
</div>
{{end}}
+25 -25
View File
@@ -10,9 +10,9 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Indítópult</h2>
<h2>{{T "launcher.inditopult"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
<button type="button" class="btn btn-outline share-open-btn" onclick="openShareModal()"><svg class="ico"><use href="#i-share"/></svg>Indítópult megosztása</button>
<button type="button" class="btn btn-outline share-open-btn" onclick="openShareModal()"><svg class="ico"><use href="#i-share"/></svg>{{T "launcher.inditopult_megosztasa"}}</button>
</div>
{{if .ShareFlash}}
@@ -44,44 +44,44 @@
</div>
{{else}}
<div class="empty-state">
<p>Még nincs telepített alkalmazás.</p>
<p><a href="/stacks" class="empty-state-link">Alkalmazások telepítése</a></p>
<p>{{T "launcher.meg_nincs_telepitett_alkalmazas"}}</p>
<p><a href="/stacks" class="empty-state-link">{{T "launcher.alkalmazasok_telepitese"}}</a></p>
</div>
{{end}}
<div class="modal-overlay" id="share-modal" style="display:none">
<div class="modal-card">
<h3>Indítópult megosztása</h3>
<p class="share-lede">Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.</p>
<h3>{{T "launcher.inditopult_megosztasa"}}</h3>
<p class="share-lede">{{T "launcher.egy_megoszthato_link_amely_csak"}}</p>
{{if .ShareEnabled}}
<div class="share-field">
<label class="share-label">Megosztási link</label>
<label class="share-label">{{T "launcher.megosztasi_link"}}</label>
<div class="share-link-row">
<input id="share-link-input" type="text" readonly value="{{.ShareURL}}">
<button type="button" id="share-copy-btn" class="btn btn-outline" onclick="copyShareLink()">Link másolása</button>
<button type="button" id="share-native-btn" class="btn btn-outline" style="display:none" onclick="nativeShare()">Megosztás…</button>
<button type="button" id="share-copy-btn" class="btn btn-outline" onclick="copyShareLink()">{{T "launcher.link_masolasa"}}</button>
<button type="button" id="share-native-btn" class="btn btn-outline" style="display:none" onclick="nativeShare()">{{T "launcher.megosztas"}}</button>
</div>
</div>
<div class="share-qr">
<img src="/launcher/share/qr.png" alt="QR-kód a megosztási linkhez" width="200" height="200">
<span class="share-qr-hint">Olvassa be telefonnal a gyors megnyitáshoz.</span>
<img src="/launcher/share/qr.png" alt="{{T "launcher.qr_kod_a_megosztasi_linkhez"}}" width="200" height="200">
<span class="share-qr-hint">{{T "launcher.olvassa_be_telefonnal_a_gyors"}}</span>
</div>
<div class="share-field">
{{if .SharePasswordSet}}
<label class="share-label">Jelszó</label>
<p class="share-note">A megosztás jelszóval védett.</p>
<label class="share-label">{{T "launcher.jelszo"}}</label>
<p class="share-note">{{T "launcher.a_megosztas_jelszoval_vedett"}}</p>
<form method="POST" action="/launcher/share/password">
{{.CSRFField}}
<input type="hidden" name="action" value="clear">
<button type="submit" class="btn btn-outline">Jelszó törlése</button>
<button type="submit" class="btn btn-outline">{{T "launcher.jelszo_torlese"}}</button>
</form>
{{else}}
<form method="POST" action="/launcher/share/password">
{{.CSRFField}}
<label class="share-label">Jelszó (nem kötelező)</label>
<label class="share-label">{{T "launcher.jelszo_nem_kotelezo"}}</label>
<div class="share-link-row">
<input type="password" name="password" autocomplete="new-password" placeholder="Legalább 8 karakter">
<button type="submit" class="btn btn-outline">Jelszó beállítása</button>
<input type="password" name="password" autocomplete="new-password" placeholder="{{T "launcher.legalabb_8_karakter"}}">
<button type="submit" class="btn btn-outline">{{T "launcher.jelszo_beallitasa"}}</button>
</div>
</form>
{{end}}
@@ -89,26 +89,26 @@
<div class="share-actions">
<form method="POST" action="/launcher/share/rotate">
{{.CSRFField}}
<button type="submit" class="btn btn-outline" data-confirm="Új link készítése? A régi link és minden korábbi belépés érvénytelenné válik.">Új link készítése</button>
<button type="submit" class="btn btn-outline" data-confirm="{{T "launcher.uj_link_keszitese_a_regi"}}">{{T "launcher.uj_link_keszitese"}}</button>
</form>
<form method="POST" action="/launcher/share/disable">
{{.CSRFField}}
<button type="submit" class="btn btn-danger" data-confirm="Biztosan kikapcsolja a megosztást? A link azonnal érvénytelenné válik.">Megosztás kikapcsolása</button>
<button type="submit" class="btn btn-danger" data-confirm="{{T "launcher.biztosan_kikapcsolja_a_megosztast_a"}}">{{T "launcher.megosztas_kikapcsolasa"}}</button>
</form>
</div>
{{else}}
<p class="share-note">A megosztás jelenleg ki van kapcsolva.</p>
<p class="share-note">{{T "launcher.a_megosztas_jelenleg_ki_van"}}</p>
<form method="POST" action="/launcher/share/enable">
{{.CSRFField}}
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="closeShareModal()">Bezárás</button>
<button type="submit" class="btn btn-primary">Megosztás bekapcsolása</button>
<button type="button" class="btn btn-outline" onclick="closeShareModal()">{{T "launcher.bezaras"}}</button>
<button type="submit" class="btn btn-primary">{{T "launcher.megosztas_bekapcsolasa"}}</button>
</div>
</form>
{{end}}
{{if .ShareEnabled}}
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="closeShareModal()">Bezárás</button>
<button type="button" class="btn btn-outline" onclick="closeShareModal()">{{T "launcher.bezaras"}}</button>
</div>
{{end}}
</div>
@@ -121,7 +121,7 @@ function copyShareLink(){
var inp=document.getElementById('share-link-input');
var btn=document.getElementById('share-copy-btn');
if(!inp)return;
function done(){if(btn){var o=btn.textContent;btn.textContent='Másolva';setTimeout(function(){btn.textContent=o;},1500);}}
function done(){if(btn){var o=btn.textContent;btn.textContent='{{T "launcher.masolva"}}';setTimeout(function(){btn.textContent=o;},1500);}}
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(inp.value).then(done,function(){inp.select();done();});}
else{inp.select();try{document.execCommand('copy');}catch(e){}done();}
}
@@ -131,7 +131,7 @@ function copyShareLink(){
function nativeShare(){
var inp=document.getElementById('share-link-input');
if(!inp||!navigator.share)return;
navigator.share({title:'Indítópult — {{.Domain}}',text:'Az otthoni alkalmazások egy helyen.',url:inp.value})
navigator.share({title:'{{T "launcher.inditopult_2"}}',text:'{{T "launcher.az_otthoni_alkalmazasok_egy_helyen"}}',url:inp.value})
.catch(function(e){if(e&&e.name==='AbortError')return;copyShareLink();});
}
(function(){
+96 -97
View File
@@ -1,6 +1,6 @@
{{define "layout_start"}}
<!DOCTYPE html>
<html lang="hu" class="no-js">
<html lang="{{T "layout.html_lang"}}" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -26,8 +26,8 @@
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';
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='{{T "layout.igen"}}';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='{{T "layout.megse"}}';
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;}
@@ -60,14 +60,14 @@
});
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</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);}
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>{{T "layout.uzenet"}}</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</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>
{{template "icon_sprite"}}
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="{{T "layout.menu"}}">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
@@ -77,47 +77,47 @@
<img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class="{{if eq .Page "launcher"}}active{{end}}"><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/launcher" class="{{if eq .Page "launcher"}}active{{end}}"><svg class="ico"><use href="#i-rocket"/></svg>{{T "layout.inditopult"}}</a></li>
<li><a href="/dashboard" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>{{T "layout.vezerlopult"}}</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>{{T "layout.alkalmazasok"}}</a></li>
{{$storageOpen := or (eq .Page "storage") (eq .Page "storage-network")}}
<li class="nav-group{{if $storageOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $storageOpen}} active{{end}}" aria-expanded="{{if $storageOpen}}true{{else}}false{{end}}" 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>
<button type="button" class="nav-group-toggle{{if $storageOpen}} active{{end}}" aria-expanded="{{if $storageOpen}}true{{else}}false{{end}}" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>{{T "layout.tarhely"}}<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="{{if eq .Page "storage"}}active{{end}}">Meghajtók</a></li>
<li><a href="/storage/network" class="{{if eq .Page "storage-network"}}active{{end}}">Hálózati tárhely</a></li>
<li><a href="/storage" class="{{if eq .Page "storage"}}active{{end}}">{{T "layout.meghajtok"}}</a></li>
<li><a href="/storage/network" class="{{if eq .Page "storage-network"}}active{{end}}">{{T "layout.halozati_tarhely"}}</a></li>
</ul>
</li>
{{$backupsOpen := or (eq .Page "backups") (eq .Page "backups-remote") (eq .Page "backups-apps") (eq .Page "backups-restore")}}
<li class="nav-group{{if $backupsOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $backupsOpen}} active{{end}}" aria-expanded="{{if $backupsOpen}}true{{else}}false{{end}}" 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>
<button type="button" class="nav-group-toggle{{if $backupsOpen}} active{{end}}" aria-expanded="{{if $backupsOpen}}true{{else}}false{{end}}" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>{{T "layout.biztonsagi_mentes"}}<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="{{if eq .Page "backups"}}active{{end}}">Áttekintés</a></li>
<li><a href="/backups/remote" class="{{if eq .Page "backups-remote"}}active{{end}}">Távoli mentés</a></li>
<li><a href="/backups/apps" class="{{if eq .Page "backups-apps"}}active{{end}}">Alkalmazások</a></li>
<li><a href="/backups/restore" class="{{if eq .Page "backups-restore"}}active{{end}}">Visszaállítás</a></li>
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}">{{T "layout.attekintes"}}</a></li>
<li><a href="/backups/remote" class="{{if eq .Page "backups-remote"}}active{{end}}">{{T "layout.tavoli_mentes"}}</a></li>
<li><a href="/backups/apps" class="{{if eq .Page "backups-apps"}}active{{end}}">{{T "layout.alkalmazasok"}}</a></li>
<li><a href="/backups/restore" class="{{if eq .Page "backups-restore"}}active{{end}}">{{T "layout.visszaallitas"}}</a></li>
</ul>
</li>
{{$sharingOpen := eq .Page "sharing"}}
<li class="nav-group{{if $sharingOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $sharingOpen}} active{{end}}" aria-expanded="{{if $sharingOpen}}true{{else}}false{{end}}" 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>
<button type="button" class="nav-group-toggle{{if $sharingOpen}} active{{end}}" aria-expanded="{{if $sharingOpen}}true{{else}}false{{end}}" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>{{T "layout.megosztas"}}<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="{{if eq .Page "sharing"}}active{{end}}">Hálózati megosztás</a></li>
<li><a href="/sharing" class="{{if eq .Page "sharing"}}active{{end}}">{{T "layout.halozati_megosztas"}}</a></li>
</ul>
</li>
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>{{T "layout.rendszermonitor"}}</a></li>
<li><a href="/debug" class="{{if eq .Page "debug"}}active{{end}}"><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<div class="nav-group-label">{{T "layout.beallitasok"}}</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class="{{if eq .Page "settings"}}active{{end}}"><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class="{{if eq .Page "settings-notifications"}}active{{end}}"><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class="{{if eq .Page "settings-security"}}active{{end}}"><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
<li><a href="/settings" class="{{if eq .Page "settings"}}active{{end}}"><svg class="ico"><use href="#i-settings"/></svg>{{T "layout.rendszer"}}</a></li>
<li><a href="/settings/notifications" class="{{if eq .Page "settings-notifications"}}active{{end}}"><svg class="ico"><use href="#i-bell"/></svg>{{T "layout.ertesitesek"}}</a></li>
<li><a href="/settings/security" class="{{if eq .Page "settings-security"}}active{{end}}"><svg class="ico"><use href="#i-lock"/></svg>{{T "layout.biztonsag_es_hozzaferes"}}</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">{{.Version}}</span>
{{if .AuthEnabled}}<a href="/logout" class="logout-link">Kijelentkezés ↗</a>{{end}}
<span class="version">{{.Version}}</span>{{if .LangSwitch}}<form method="POST" action="/settings/language" class="lang-switch">{{.CSRFField}}<input type="hidden" name="back" value="{{.LangSwitchBack}}"><button type="submit" name="lang" value="hu" class="lang-switch-btn{{if eq .Lang "hu"}} active{{end}}" lang="hu">Magyar</button><button type="submit" name="lang" value="en" class="lang-switch-btn{{if eq .Lang "en"}} active{{end}}" lang="en">English</button></form>{{end}}
{{if .AuthEnabled}}<a href="/logout" class="logout-link">{{T "layout.kijelentkezes"}}</a>{{end}}
</div>
</div>
</nav>
@@ -126,7 +126,7 @@
<div class="alerts-container">
<div class="alert-banner alert-banner-error">
<span class="alert-icon"><svg class="ico"><use href="#i-triangle-alert"/></svg></span>
<span class="alert-message">A vezérlőpult még nincs jelszóval védve — a beállító kódot hamarosan e-mailben küldjük.</span>
<span class="alert-message">{{T "layout.a_vezerlopult_meg_nincs_jelszoval"}}</span>
</div>
</div>
{{end}}
@@ -146,28 +146,28 @@
reachable state: a fresh escrow ceremony during the window replaces the package, which
is the exact act that cost both demo boxes their history on 2026-08-04. A countdown
started before this shipped carries no pin and takes the cautious branch. */}}
A korábbi távoli mentéseidet <strong>{{.RecoveryAbandonDays}} nap múlva</strong> ({{.RecoveryAbandonDate}}) véglegesen töröljük, a kérésed szerint.
{{if .RecoveryAbandonRetrievalOffered}}Addig még visszaszerezheted őket a helyreállítási kóddal.{{else}}Hogy ezek még visszaszerezhetők-e a helyreállítási kóddal, azt innen nem tudjuk megállapítani — ha vissza szeretnéd kapni őket, <strong>írj nekünk a törlés előtt</strong>.{{end}}
{{T "layout.a_korabbi_tavoli_menteseidet_nap"}}
{{if .RecoveryAbandonRetrievalOffered}}{{T "layout.addig_meg_visszaszerezheted_oket_a"}}{{else}}{{T "layout.hogy_ezek_meg_visszaszerezhetok_e"}}{{end}}
{{else if ge .RecoveryReminderTier 14}}
<strong>Két hete</strong> várnak rád a korábbi távoli mentéseid, és még nem adtad meg a helyreállítási kódodat. Amíg nem teszed, ezekhez a mentésekhez nem férsz hozzá.
{{T "layout.ket_hete_varnak_rad_a"}}
{{else if ge .RecoveryReminderTier 7}}
Már <strong>egy hete</strong> megvannak a korábbi távoli mentéseid, de a helyreállítási kódod nélkül nem tudjuk megnyitni őket.
{{T "layout.mar_egy_hete_megvannak_a"}}
{{else if ge .RecoveryReminderTier 3}}
A korábbi távoli mentéseid megvannak — a megnyitásukhoz a helyreállítási kódod szükséges.
{{T "layout.a_korabbi_tavoli_menteseid_megvannak"}}
{{else}}
A korábbi távoli mentéseid megvannak, de ehhez a géphez a helyreállítási kódod szükséges.
{{T "layout.a_korabbi_tavoli_menteseid_megvannak_2"}}
{{end}}
<a href="/recovery">Megnézem</a>
<a href="/recovery">{{T "layout.megnezem"}}</a>
</span>
<span class="alert-actions">
<form method="POST" action="/recovery/banner/dismiss" style="display:inline">
{{.CSRFField}}<input type="hidden" name="back" value="{{.RecoveryBannerBack}}">
<button type="submit" class="btn btn-sm btn-outline">Most nem</button>
<button type="submit" class="btn btn-sm btn-outline">{{T "layout.most_nem"}}</button>
</form>
{{if not .RecoveryAbandonDays}}
<form method="POST" action="/recovery/remind-optout" style="display:inline">
{{.CSRFField}}<input type="hidden" name="back" value="{{.RecoveryBannerBack}}">
<button type="submit" class="btn btn-sm btn-outline">Ne emlékeztessen újra</button>
<button type="submit" class="btn btn-sm btn-outline">{{T "layout.ne_emlekeztessen_ujra"}}</button>
</form>
{{end}}
</span>
@@ -183,13 +183,12 @@
<div class="alert-banner alert-banner-warning">
<span class="alert-icon"><svg class="ico"><use href="#i-triangle-alert"/></svg></span>
<span class="alert-message">
A távoli mentés szünetel, amíg nem hozod létre a helyreállítási kódot.
<a href="/backup/escrow"><strong>Helyreállítási kód létrehozása &rarr;</strong></a>
{{T "layout.a_tavoli_mentes_szunetel_amig"}}
</span>
<span class="alert-actions">
<form method="POST" action="/backup/escrow/banner/dismiss" style="display:inline">
{{.CSRFField}}<input type="hidden" name="back" value="{{.EscrowBannerBack}}">
<button type="submit" class="btn btn-sm btn-outline">Most nem</button>
<button type="submit" class="btn btn-sm btn-outline">{{T "layout.most_nem"}}</button>
</form>
</span>
</div>
@@ -244,7 +243,7 @@
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.');
showAlert('{{T "layout.ez_az_alkalmazas_mar_telepitve"}}');
window.location.reload();
return false;
}
@@ -257,7 +256,7 @@
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.innerHTML = '{{T "layout.frissites"}}';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
@@ -266,7 +265,7 @@
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.textContent = data.ok ? (data.message || '{{T "layout.sablonok_frissitve"}}') : ('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.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);
@@ -276,7 +275,7 @@
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.textContent = '{{T "layout.halozati_hiba"}} ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
@@ -290,7 +289,7 @@
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.textContent = '{{T "layout.folyamatban"}}';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
@@ -299,7 +298,7 @@
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
@@ -314,7 +313,7 @@
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
@@ -337,7 +336,7 @@
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.innerHTML = '<div class="modal-card"><h3>{{T "layout.betoltes"}}</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
@@ -346,26 +345,26 @@
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>';
hddInfo = '<div class="modal-hdd-info"><strong>{{T "layout.felhasznaloi_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 class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : '{{T "layout.nem_letezik"}}') + ')</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>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> {{T "layout.felhasznaloi_adatok_torlese_a_merevlemez"}}</label>';
}
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>' +
'<h3>{{T "layout.alkalmazas_torlese"}} ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">{{T "layout.ez_a_muvelet_eltavolitja_a"}}</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">{{T "layout.ez_a_muvelet_nem_visszavonhato"}}</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>' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">{{T "layout.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">{{T "layout.torles"}}</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>';
'<h3>{{T "layout.hiba_2"}}</h3><p style="color:var(--text-2)">{{T "layout.nem_sikerult_lekerni_az_adatokat"}} ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">{{T "layout.bezaras"}}</button></div>';
}
}
function closeDeleteModal() {
@@ -377,7 +376,7 @@
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
btn.textContent = '{{T "layout.torles_folyamatban"}}';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
@@ -389,36 +388,36 @@
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>';
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">{{T "layout.torolt_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>';
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">{{T "layout.megorzott_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>';
}
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>' +
'<h3>{{T "layout.sikeresen_torolve"}}</h3>' +
'<p style="color:var(--text-2)">{{T "layout.az_alkalmazas"}}' + name + '{{T "layout.torolve_lett"}}</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">{{T "layout.bezaras"}}</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.disabled = false;
btn.textContent = 'Törlés';
btn.textContent = '{{T "layout.torles"}}';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
btn.textContent = '{{T "layout.torles"}}';
}
}
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.innerHTML = '<div class="modal-card"><h3>{{T "layout.betoltes"}}</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
@@ -429,24 +428,24 @@
var sections = '';
// Section 1: Always removed
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<strong>{{T "layout.mindig_torlodik"}}</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>' +
'<li>{{T "layout.docker_kotetek_adatbazis_alkalmazas_konf"}}</li>' +
'<li>{{T "layout.telepitesi_konfiguracio_app_yaml"}}</li>' +
'<li>{{T "layout.masodlagos_mentes_utemezese"}}</li>' +
'</ul></div>';
// Section 2: HDD data
var hddCheckbox = '';
if (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>';
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : '{{T "layout.nem_letezik"}}') + ')</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>' +
'<strong>{{T "layout.felhasznaloi_adatok_a_merevlemezen"}}</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> {{T "layout.felhasznaloi_adatok_torlese"}}</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíti az alkalmazást, az adatokat újra importálnia kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'{{T "layout.ha_ujratelepiti_az_alkalmazast_az"}}' +
'</div></div>';
hddCheckbox = '<script>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
@@ -459,26 +458,26 @@
});
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>' +
'<strong>{{T "layout.mentesi_adatok"}}</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> {{T "layout.mentesi_adatok_torlese"}}</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.' +
'{{T "layout.az_ejszakai_restic_pillanatkepek_nem"}}' +
'</div></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>' +
'<h3>{{T "layout.alkalmazas_eltavolitasa"}} ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">{{T "layout.az_alkalmazas_visszaall_nincs_telepitve"}}</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">{{T "layout.ez_a_muvelet_nem_visszavonhato"}}</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>' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">{{T "layout.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">{{T "layout.eltavolitas"}}</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>';
'<h3>{{T "layout.hiba_2"}}</h3><p style="color:var(--text-2)">{{T "layout.nem_sikerult_lekerni_az_adatokat"}} ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">{{T "layout.bezaras"}}</button></div>';
}
}
function closeRemoveModal() {
@@ -492,7 +491,7 @@
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
btn.textContent = '{{T "layout.eltavolitas_folyamatban"}}';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
@@ -504,35 +503,35 @@
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>';
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">{{T "layout.torolt_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>';
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">{{T "layout.torolt_mentesek"}} ' + 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>';
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">{{T "layout.megorzott_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.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>';
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">{{T "layout.nem_torolt_mentesek"}} ' + 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>' +
'<h3>{{T "layout.sikeresen_eltavolitva"}}</h3>' +
'<p style="color:var(--text-2)">{{T "layout.az_alkalmazas"}}' + name + '{{T "layout.eltavolitva_ujratelepitheto_a_telepites"}}</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">{{T "layout.bezaras"}}</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
btn.textContent = '{{T "layout.eltavolitas"}}';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
btn.textContent = '{{T "layout.eltavolitas"}}';
}
}
</script>
@@ -134,6 +134,11 @@ body {
font-size: .8rem;
}
.version { color: var(--text-3); }
/* i18n (v0.247.0): the language switch. Language names are shown in their own language, never translated. */
.lang-switch { display: inline-flex; gap: 2px; margin: 0; }
.lang-switch-btn { background: none; border: 1px solid var(--line); border-radius: var(--radius); color: var(--text-3); font: inherit; font-size: .75rem; padding: 0 .35rem; cursor: pointer; }
.lang-switch-btn:hover { color: var(--blue-bright); }
.lang-switch-btn.active { color: var(--text-1); border-color: var(--blue); }
.logout-link { color: var(--text-3); text-decoration: none; transition: color 0.2s ease; }
.logout-link:hover { color: var(--blue-bright); }
/* Mobile chrome (v0.166.0) — hidden by default so desktop (>768px) stays pixel-identical; the