v0.248.0: i18n slice 1 release A — apps and settings in English, Hungarian byte-identical (R-556)
gates / gates (push) Successful in 20s

Ten pages converted against fixtures captured from unconverted templates (6be55a0); marker
coverage test; English page mask narrowed; English retrieval-promise stems; common keys;
title keys and infraMeta from the bundle; old wording tests read the Hungarian expansion.

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 17:36:08 +02:00
parent 6be55a0eb3
commit ff68b0b433
30 changed files with 1690 additions and 651 deletions
@@ -20,7 +20,7 @@ import (
// now hit twice (R-262, and the OOB fixture in hub v0.99.0).
func diskMeterBlock(t *testing.T) string {
t.Helper()
raw, err := templateFS.ReadFile("templates/dashboard.html")
raw, err := huTemplateSource("templates/dashboard.html")
if err != nil {
t.Fatalf("read dashboard.html: %v", err)
}
+6
View File
@@ -176,6 +176,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
sysInfo := system.GetInfo(s.primaryHDDPath(), s.cpuCollector)
data := s.baseData("dashboard", "Vezérlőpult")
data["TitleKey"] = "page.title.dashboard" // i18n: the Hungarian title above is what hu renders
s.addRecoveryBanner(data, r) // R-241: the reminder bar, per visit
data["SettingsWarning"] = s.settings.LoadWarning // non-empty if settings.json was recovered from corruption
data["Stacks"] = deployedStacks
@@ -356,6 +357,7 @@ func visibleCatalogStacks(in []stacks.Stack) []stacks.Stack {
func (s *Server) stacksHandler(w http.ResponseWriter, r *http.Request) {
data := s.baseData("stacks", "Alkalmazások")
data["TitleKey"] = "page.title.stacks" // i18n: the Hungarian title above is what hu renders
allStacks := visibleCatalogStacks(s.stackMgr.GetStacks())
data["Stacks"] = allStacks
data["MissingStorage"] = s.missingStorageMap(allStacks)
@@ -793,6 +795,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
func (s *Server) monitoringHandler(w http.ResponseWriter, r *http.Request) {
data := s.baseData("monitoring", "Rendszermonitor")
data["TitleKey"] = "page.title.monitoring" // i18n: the Hungarian title above is what hu renders
data["SystemInfo"] = system.GetInfo(s.primaryHDDPath(), s.cpuCollector)
data["StorageBars"] = s.buildStorageBars()
@@ -2043,6 +2046,7 @@ func (s *Server) settingsBaseData(page, title string) map[string]interface{} {
// controller + server restart.
func (s *Server) systemPageData() map[string]interface{} {
data := s.settingsBaseData("settings", "Beállítások")
data["TitleKey"] = "page.title.settings" // i18n: the Hungarian title above is what hu renders
data["GitRepoURL"] = s.cfg.Git.RepoURL
data["GitSyncInterval"] = s.cfg.Git.SyncInterval
data["BackupEnabled"] = s.cfg.Backup.Enabled
@@ -2157,6 +2161,7 @@ func (s *Server) networkStoragePageData() map[string]interface{} {
// notificationsPageData builds the Értesítések subpage: notification prefs + app-email.
func (s *Server) notificationsPageData() map[string]interface{} {
data := s.settingsBaseData("settings-notifications", "Értesítések")
data["TitleKey"] = "page.title.settings_notifications" // i18n: the Hungarian title above is what hu renders
data["HubEnabled"] = s.cfg.Hub.Enabled
data["NotificationPrefs"] = s.settings.GetNotificationPrefs()
@@ -2173,6 +2178,7 @@ func (s *Server) notificationsPageData() map[string]interface{} {
// emergency/recovery info.
func (s *Server) securityPageData() map[string]interface{} {
data := s.settingsBaseData("settings-security", "Biztonság és hozzáférés")
data["TitleKey"] = "page.title.settings_security" // i18n: the Hungarian title above is what hu renders
// Recovery info for emergency section.
//
+18 -3
View File
@@ -35,6 +35,7 @@ import (
// TestI18nParity fails naming the page and the first differing line.
var updateI18nGolden = flag.Bool("update-i18n-golden", false, "write MISSING testdata/i18n_parity fixtures from the current templates (an existing fixture is never overwritten)")
var i18nDumpDir = flag.String("i18n-dump-dir", "", "TestI18nEnglishPages: also write every case's hu and en render to this directory (evidence, per-page review)")
var i18nGoldenOnly = flag.String("i18n-golden-only", "", "with -update-i18n-golden: only cases whose name matches this regexp")
var relativeAgeRe = regexp.MustCompile(`\d+ (napja|órája|perce)`)
@@ -52,6 +53,12 @@ func i18nFixtureTime() time.Time { return time.Date(2026, 9, 14, 3, 12, 0, 0, ti
var i18nTitleKeys = map[string]string{
"Indítópult": "page.title.launcher",
"Biztonsági mentés": "page.title.backups",
"Vezérlőpult": "page.title.dashboard",
"Alkalmazások": "page.title.stacks",
"Rendszermonitor": "page.title.monitoring",
"Beállítások": "page.title.settings",
"Értesítések": "page.title.settings_notifications",
"Biztonság és hozzáférés": "page.title.settings_security",
}
func i18nLayoutData(page, title string) map[string]interface{} {
@@ -376,7 +383,7 @@ func fixtureStrings(v reflect.Value, out map[string]bool) {
}
}
var i18nKeyLikeRe = regexp.MustCompile(`\b(launcher|backups|backups_shared|app_info|layout|func)\.[a-z0-9_]+\b`)
var i18nKeyLikeRe = regexp.MustCompile(`\b[a-z][a-z_]*\.[a-z0-9_.]+\b`)
var emptyElemRe = regexp.MustCompile(`<(h[1-6]|button|label|li|strong|option|p)(\s[^>]*)?>\s*</(h[1-6]|button|label|li|strong|option|p)>`)
// TestI18nEnglishPages — the three pages in English: no Hungarian letter from the TEMPLATE copy, no
@@ -395,14 +402,22 @@ func TestI18nEnglishPages(t *testing.T) {
for _, c := range i18nCases() {
hu := renderI18nCase(t, s, "hu", c)
en := renderI18nCase(t, s, "en", c)
if *i18nDumpDir != "" {
_ = os.MkdirAll(*i18nDumpDir, 0o755)
_ = os.WriteFile(filepath.Join(*i18nDumpDir, c.name+".hu.html"), []byte(hu), 0o644)
_ = os.WriteFile(filepath.Join(*i18nDumpDir, c.name+".en.html"), []byte(en), 0o644)
}
if !huLetter(hu) {
t.Fatalf("%s: positive control failed — the Hungarian render carries no Hungarian letter", c.name)
}
if strings.Contains(en, `{{T `) {
t.Errorf("%s: an unexpanded marker reached the English page", c.name)
}
if m := i18nKeyLikeRe.FindString(en); m != "" && s.i18n.Has(i18n.Default, m) {
t.Errorf("%s: a raw bundle key reached the English page: %q", c.name, m)
for _, m := range i18nKeyLikeRe.FindAllString(en, -1) {
if s.i18n.Has(i18n.Default, m) {
t.Errorf("%s: a raw bundle key reached the English page: %q", c.name, m)
break
}
}
if a, b := len(emptyElemRe.FindAllString(hu, -1)), len(emptyElemRe.FindAllString(en, -1)); b > a {
t.Errorf("%s: English has %d empty elements, Hungarian %d — a translation rendered blank", c.name, b, a)
+16
View File
@@ -129,6 +129,22 @@ func (s *Server) localeFuncs(lang string) template.FuncMap {
}
return lt.Format("2006-01-02") + " " + hm
},
// infraMeta: the protected infra stacks' curated identity (inframeta.go). Names that are product
// names stay; descriptions — and samba's display name — come from the bundle.
"infraMeta": func(name string) *InfraMeta {
m := infraMetaFor(name)
if m == nil {
return nil
}
out := *m
if v, _, ok := b.Text(lang, "func.infra."+name+".description"); ok {
out.Description = v
}
if v, _, ok := b.Text(lang, "func.infra."+name+".display"); ok {
out.DisplayName = v
}
return &out
},
"statusText": func(status string) string {
switch status {
case "pending", "restoring", "done", "failed", "skipped":
@@ -156,6 +156,15 @@ func TestLocaleFuncsHungarianBundleMatchesFuncMap(t *testing.T) {
if got, want := b.Msg("hu", "func.time.yesterday"), timeAgo(now.Add(-30*time.Hour)); got != want {
t.Errorf("yesterday: %q vs %q", got, want)
}
// infraMeta (slice 1): the Hungarian bundle carries the same curated words as inframeta.go.
for name, m := range infraMetaMap {
if got := b.Msg("hu", "func.infra."+name+".description"); got != m.Description {
t.Errorf("infra %s description: hu.json %q, inframeta.go %q", name, got, m.Description)
}
if k := "func.infra." + name + ".display"; b.Has("hu", k) && b.Msg("hu", k) != m.DisplayName {
t.Errorf("infra %s display: hu.json %q, inframeta.go %q", name, b.Msg("hu", k), m.DisplayName)
}
}
// And the English funcs really are English.
en := s.localeFuncs("en")
if got := en["timeAgo"].(func(time.Time) string)(now.Add(-time.Minute - time.Second)); got != "1 minute ago" {
@@ -238,3 +247,19 @@ func TestLanguageSwitch_EndToEnd(t *testing.T) {
t.Error("back on Hungarian: the page must be Hungarian with no switch")
}
}
// huTemplateSource returns an embedded template AS THE HUNGARIAN SET PARSES IT — markers expanded. Tests
// that assert on a template's Hungarian wording read this, never the raw file: since slice 1 the
// wording lives in hu.json, and a raw read would find a marker where the sentence used to be.
func huTemplateSource(name string) ([]byte, error) {
raw, err := templateFS.ReadFile(name)
if err != nil {
return nil, err
}
b, err := i18n.Shared()
if err != nil {
return nil, err
}
out, _ := b.Expand(i18n.Default, string(raw))
return []byte(out), nil
}
+1 -1
View File
@@ -92,7 +92,7 @@ func TestNetworkStorageWarningsIn_UnreachableAloneUnchanged(t *testing.T) {
// must never reuse the recoverable-unreachable copy; the unreachable line stays byte-identical).
func TestNetworkBadgeTemplates_DistinctStrings(t *testing.T) {
for _, tpl := range []string{"templates/dashboard.html", "templates/stacks.html"} {
body, err := os.ReadFile(tpl)
body, err := huTemplateSource(tpl)
if err != nil {
t.Fatal(err)
}
@@ -114,7 +114,7 @@ func TestSettingsSectionInventory(t *testing.T) {
}
var union strings.Builder
for _, f := range []string{"settings_system.html", "settings_notifications.html", "settings_security.html", "storage.html", "storage_network.html"} {
b, err := templateFS.ReadFile("templates/" + f)
b, err := huTemplateSource("templates/" + f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
@@ -224,7 +224,7 @@ func TestStorageNoNativeConfirm(t *testing.T) {
// allow the pre-existing type-to-confirm overlay helper names
allow := regexp.MustCompile(`openConfirm|__closeConfirm|confirmEject|confirmWipe|typeToConfirm|confirm-`)
for _, f := range []string{"storage.html", "storage_network.html", "settings_system.html", "settings_notifications.html", "settings_security.html"} {
b, err := templateFS.ReadFile("templates/" + f)
b, err := huTemplateSource("templates/" + f)
if err != nil {
t.Fatalf("read %s: %v", f, err)
}
@@ -239,7 +239,7 @@ func TestStorageNoNativeConfirm(t *testing.T) {
// TestStorageAgentDownNote (D1 Scenario C, static): the enrichment JS has an error path that
// renders the exact warn note into #agent-warn-note (graceful degradation when the agent is down).
func TestStorageAgentDownNote(t *testing.T) {
b, err := templateFS.ReadFile("templates/storage.html")
b, err := huTemplateSource("templates/storage.html")
if err != nil {
t.Fatal(err)
}
@@ -282,7 +282,7 @@ func TestNoEmojiInTemplates(t *testing.T) {
if !strings.HasSuffix(e.Name(), ".html") {
continue
}
b, err := templateFS.ReadFile("templates/" + e.Name())
b, err := huTemplateSource("templates/" + e.Name())
if err != nil {
t.Fatal(err)
}
@@ -342,7 +342,7 @@ func TestStorageInit_PollsAgentFormatStatusOnTimeout(t *testing.T) {
// /storage, not /settings. One assertion per template.
func TestStorageWizardBackAnchors_PointToStorage(t *testing.T) {
for _, file := range []string{"templates/storage_init.html", "templates/storage_attach.html"} {
b, err := templateFS.ReadFile(file)
b, err := huTemplateSource(file)
if err != nil {
t.Fatalf("read %s: %v", file, err)
}
@@ -3,15 +3,15 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">&larr; Alkalmazások</a>
<h2>{{.Stack.Meta.DisplayName}} &mdash; Exportálás</h2>
<a href="/stacks" class="btn btn-sm btn-outline">{{T "app_export.alkalmazasok"}}</a>
<h2>{{T "app_export.exportalas"}}</h2>
</div>
</div>
<div class="card" style="max-width:700px">
<h3>Mentés helye</h3>
<h3>{{T "app_export.mentes_helye"}}</h3>
<select id="destDrive" onchange="loadEstimate()" style="width:100%;padding:.5rem;margin-bottom:1rem">
<option value="">Válassz tárolót...</option>
<option value="">{{T "app_export.valassz_tarolot"}}</option>
{{range .Drives}}
<option value="{{.Path}}">{{if .Label}}{{.Label}} ({{.Path}}){{else}}{{.Path}}{{end}}</option>
{{end}}
@@ -19,19 +19,19 @@
<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>Konfiguráció:</span>
<span>{{T "app_export.konfiguracio"}}</span>
<span id="estConfig">-</span>
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
<span>Felhasználói adatok:</span>
<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>Összesen:</span>
<span><span id="estTotal">-</span> <span id="estFree" style="color:var(--text-3)">(szabad: -)</span></span>
<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>Becsült idő:</span>
<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>
@@ -39,53 +39,53 @@
<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">Mindig része a mentésnek:</div>
<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">Választható tartalom:</div>
<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">A kihagyott mappák tartalma a Fájlkezelőben bármikor elérhető, és külön is lementhető.</div>
<button type="button" class="btn btn-xs btn-outline" onclick="toggleExcl()" id="exclToggle">Kihagyott tartalom megjelenítése</button>
<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>Jelszó (opcionális)</h3>
<h3>{{T "app_export.jelszo_opcionalis"}}</h3>
<div style="display:flex;gap:.5rem;margin-bottom:1rem">
<input type="password" id="exportPassword" placeholder="Titkosítási jelszó" style="flex:1;padding:.5rem">
<button class="btn btn-sm btn-outline" onclick="togglePw()" type="button" title="Jelszó mutatása">&#128065;</button>
<input type="password" id="exportPassword" placeholder="{{T "app_export.titkositasi_jelszo"}}" style="flex:1;padding:.5rem">
<button class="btn btn-sm btn-outline" onclick="togglePw()" type="button" title="{{T "app_export.jelszo_mutatasa"}}">&#128065;</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>Alkalmazás leállítása mentés előtt (ajánlott)</strong>
<div style="color:var(--text-3);font-size:.85rem">Az adatok konzisztenciája érdekében javasolt leállítani az alkalmazást mentés közben.</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>
Exportálás indítása
{{T "app_export.exportalas_inditasa"}}
</button>
</div>
<div id="progressCard" class="card" style="max-width:700px;display:none">
<h3>Folyamat</h3>
<h3>{{T "app_export.folyamat"}}</h3>
<div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div>
<div id="doneCard" class="card" style="max-width:700px;display:none">
<h3 style="color:var(--blue)">Kész!</h3>
<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">Megnyitás FileBrowser-ben &nearr;</a>
<a id="doneFBLink" href="#" target="_blank" class="btn btn-outline">{{T "app_export.megnyitas_filebrowser_ben"}}</a>
</div>
<script>
@@ -126,12 +126,12 @@ async function loadEstimate() {
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 = '(szabad: ' + est.dest_free_human + ')';
document.getElementById('estTime').textContent = '~' + est.estimated_minutes + ' perc';
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 = 'Nincs elég szabad hely a kiválasztott tárolón!';
warn.textContent = '{{T "app_export.nincs_eleg_szabad_hely_a"}}';
warn.style.display = 'block';
btn.disabled = true;
} else {
@@ -200,14 +200,14 @@ function recomputeTotal() {
var full = alap + exclAll;
var s = document.getElementById('exclSummary');
if ((lastEst.excluded_items||[]).length) {
s.innerHTML = 'Alap mentés: ~' + humanBytes(alap) + '. A kihagyott, nagy méretű tartalommal együtt: ~' + humanBytes(full) + '.';
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 ? 'Kihagyott tartalom megjelenítése' : 'Kihagyott tartalom elrejtése';
document.getElementById('exclToggle').textContent = shown ? '{{T "app_export.kihagyott_tartalom_megjelenitese"}}' : '{{T "app_export.kihagyott_tartalom_elrejtese"}}';
}
function gatherSelections() {
var deselect = [], optIn = [];
@@ -242,7 +242,7 @@ async function startExport() {
});
var data = await resp.json();
if (!data.ok) {
showError(data.error || 'Hiba történt');
showError(data.error || '{{T "common.hiba_tortent"}}');
return;
}
pollStatus();
@@ -3,22 +3,22 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">&larr; Alkalmazások</a>
<h2>Alkalmazás importálás</h2>
<a href="/stacks" class="btn btn-sm btn-outline">{{T "app_import.alkalmazasok"}}</a>
<h2>{{T "app_import.alkalmazas_importalas"}}</h2>
</div>
</div>
<!-- Browser .fab upload (v0.128.0) — chunked, because the Cloudflare tunnel caps request bodies (~100 MB) -->
<div class="card" style="max-width:900px;margin-bottom:1rem">
<div id="fabUploadZone" style="border:1px dashed var(--line);border-radius:var(--radius);padding:1.25rem;text-align:center;cursor:pointer" onclick="fabPick()">
<p style="color:var(--text-2);margin:0 0 .75rem">.fab csomag feltöltése &mdash; húzza ide, vagy válassza ki a fájlt</p>
<button type="button" class="btn btn-sm btn-outline">Fájl kiválasztása</button>
<p style="color:var(--text-2);margin:0 0 .75rem">{{T "app_import.fab_csomag_feltoltese_huzza_ide"}}</p>
<button type="button" class="btn btn-sm btn-outline">{{T "app_import.fajl_kivalasztasa"}}</button>
<input type="file" id="fabFileInput" accept=".fab" style="display:none">
</div>
<div id="fabUpProgress" style="display:none;margin-top:1rem">
<div style="display:flex;justify-content:space-between;align-items:center;gap:1rem">
<span id="fabUpText" style="color:var(--text-2)">Feltöltés: 0%</span>
<button type="button" class="btn btn-sm btn-outline" onclick="fabCancel()">Megszakítás</button>
<span id="fabUpText" style="color:var(--text-2)">{{T "app_import.feltoltes_0"}}</span>
<button type="button" class="btn btn-sm btn-outline" onclick="fabCancel()">{{T "app_import.megszakitas"}}</button>
</div>
<div style="background:var(--bg-2);border-radius:var(--radius);height:6px;margin-top:.5rem;overflow:hidden">
<div id="fabUpBar" style="background:var(--blue);height:100%;width:0%"></div>
@@ -29,19 +29,19 @@
{{if not .Bundles}}
<div class="card" style="max-width:700px">
<p style="color:var(--text-3)">Nem található .fab csomag a regisztrált tárolókon.</p>
<p style="color:var(--text-3);font-size:.85rem">Exportálj egy alkalmazást az alkalmazás oldaláról, tölts fel egy .fab fájlt itt fent, vagy másolj egyet a <code>{tároló}/felhom-data/exports/</code> könyvtárba.</p>
<p style="color:var(--text-3)">{{T "app_import.nem_talalhato_fab_csomag_a"}}</p>
<p style="color:var(--text-3);font-size:.85rem">{{T "app_import.exportalj_egy_alkalmazast_az_alkalmazas"}}</p>
</div>
{{else}}
<div class="card" style="max-width:900px">
<table class="table" style="width:100%">
<thead>
<tr>
<th>Alkalmazás</th>
<th>Dátum</th>
<th>Méret</th>
<th>Tároló</th>
<th>Titkos</th>
<th>{{T "app_import.alkalmazas"}}</th>
<th>{{T "app_import.datum"}}</th>
<th>{{T "app_import.meret"}}</th>
<th>{{T "app_import.tarolo"}}</th>
<th>{{T "app_import.titkos"}}</th>
<th></th>
</tr>
</thead>
@@ -53,7 +53,7 @@
<td>{{.SizeHuman}}</td>
<td>{{if .DriveLabel}}{{.DriveLabel}}{{else}}{{.DrivePath}}{{end}}</td>
<td>{{if .Encrypted}}&#128274;{{end}}</td>
<td><button class="btn btn-sm btn-outline" onclick="showPreview('{{.Path}}', {{.Encrypted}})">Részletek &raquo;</button></td>
<td><button class="btn btn-sm btn-outline" onclick="showPreview('{{.Path}}', {{.Encrypted}})">{{T "app_import.reszletek"}}</button></td>
</tr>
{{end}}
</tbody>
@@ -63,13 +63,13 @@
<!-- Preview / password modal -->
<div id="previewCard" class="card" style="max-width:700px;display:none">
<h3 id="previewTitle">Csomag részletei</h3>
<h3 id="previewTitle">{{T "app_import.csomag_reszletei"}}</h3>
<div id="passwordPrompt" style="display:none;margin-bottom:1rem">
<p>Ez a csomag jelszóval védett. Kérlek add meg a jelszót:</p>
<p>{{T "app_import.ez_a_csomag_jelszoval_vedett"}}</p>
<div style="display:flex;gap:.5rem">
<input type="password" id="importPassword" class="form-input" autocomplete="off" placeholder="Jelszó" style="flex:1">
<button class="btn btn-primary" onclick="loadManifest()">Megnyitás</button>
<input type="password" id="importPassword" class="form-input" autocomplete="off" placeholder="{{T "common.jelszo"}}" style="flex:1">
<button class="btn btn-primary" onclick="loadManifest()">{{T "app_import.megnyitas"}}</button>
</div>
<div id="passwordError" style="display:none;color:var(--crit);margin-top:.5rem"></div>
</div>
@@ -77,52 +77,52 @@
<div id="manifestInfo" style="display:none">
<div style="background:var(--bg-2);border-radius:8px;padding:1rem;margin-bottom:1rem">
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
<span>Alkalmazás:</span>
<span>{{T "app_import.alkalmazas_2"}}</span>
<strong id="mfName">-</strong>
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
<span>Exportálva:</span>
<span>{{T "app_import.exportalva"}}</span>
<span id="mfDate">-</span>
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
<span>Controller verzió:</span>
<span>{{T "app_import.controller_verzio"}}</span>
<span id="mfVersion">-</span>
</div>
<div id="mfDBRow" style="display:flex;justify-content:space-between;margin-bottom:.25rem">
<span>Adatbázis:</span>
<span>{{T "app_import.adatbazis"}}</span>
<span id="mfDB">-</span>
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
<span>Adatok:</span>
<span>{{T "app_import.adatok"}}</span>
<span id="mfDataType">-</span>
</div>
<div style="display:flex;justify-content:space-between">
<span>Méret:</span>
<span>{{T "app_import.meret_2"}}</span>
<span id="mfSize">-</span>
</div>
</div>
<div id="overwriteWarning" style="display:none;background:var(--warning-bg, #fff3cd);border:1px solid var(--warning-border, #ffc107);border-radius:8px;padding:1rem;margin-bottom:1rem">
<strong>&#9888; FIGYELEM:</strong> A meglévő <span id="overwriteAppName"></span> alkalmazás konfigurációja és összes adata felül lesz írva!
{{T "app_import.figyelem_a_meglevo"}} <span id="overwriteAppName"></span> {{T "app_import.alkalmazas_konfiguracioja_es_osszes_adat"}}
</div>
<button id="importBtn" class="btn btn-primary" onclick="startImport()" style="width:100%">
Visszaállítás indítása
{{T "app_import.visszaallitas_inditasa"}}
</button>
</div>
</div>
<!-- Progress -->
<div id="progressCard" class="card" style="max-width:700px;display:none">
<h3>Importálás folyamata</h3>
<h3>{{T "app_import.importalas_folyamata"}}</h3>
<div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div>
<div id="doneCard" class="card" style="max-width:700px;display:none">
<h3 style="color:var(--blue)">Importálás kész!</h3>
<p>Az alkalmazás sikeresen visszaállítva.</p>
<a id="doneLink" href="/stacks" class="btn btn-primary">Alkalmazások megtekintése</a>
<h3 style="color:var(--blue)">{{T "app_import.importalas_kesz"}}</h3>
<p>{{T "app_import.az_alkalmazas_sikeresen_visszaallitva"}}</p>
<a id="doneLink" href="/stacks" class="btn btn-primary">{{T "app_import.alkalmazasok_megtekintese"}}</a>
</div>
<script>
@@ -170,7 +170,7 @@ async function loadManifest() {
if (!data.ok) {
if (selectedEncrypted) {
document.getElementById('passwordError').textContent = data.error || 'Hibás jelszó';
document.getElementById('passwordError').textContent = data.error || '{{T "app_import.hibas_jelszo"}}';
document.getElementById('passwordError').style.display = 'block';
}
return;
@@ -193,7 +193,7 @@ function showManifest(m) {
document.getElementById('passwordPrompt').style.display = 'none';
document.getElementById('manifestInfo').style.display = 'block';
document.getElementById('previewTitle').textContent = (m.display_name || m.app_name) + ' — Csomag részletei';
document.getElementById('previewTitle').textContent = (m.display_name || m.app_name) + ' {{T "app_import.csomag_reszletei_2"}}';
document.getElementById('mfName').textContent = m.display_name || m.app_name;
document.getElementById('mfDate').textContent = m.exported_at ? new Date(m.exported_at).toLocaleString('hu-HU') : '-';
document.getElementById('mfVersion').textContent = m.controller_version || '-';
@@ -208,7 +208,7 @@ function showManifest(m) {
var dataType = [];
if (m.has_hdd_data) dataType.push('HDD');
if (m.has_volume_data) dataType.push('Docker volume');
document.getElementById('mfDataType').textContent = dataType.length ? dataType.join(', ') : 'Nincs';
document.getElementById('mfDataType').textContent = dataType.length ? dataType.join(', ') : '{{T "app_import.nincs"}}';
// Format size
var bytes = m.total_size_bytes || 0;
@@ -240,7 +240,7 @@ async function startImport() {
});
var data = await resp.json();
if (!data.ok) {
showError(data.error || 'Hiba történt');
showError(data.error || '{{T "common.hiba_tortent"}}');
return;
}
pollStatus();
@@ -269,7 +269,7 @@ function pollStatus() {
document.getElementById('doneCard').style.display = 'block';
if (data.stack_name) {
document.getElementById('doneLink').href = '/stacks/' + data.stack_name + '/deploy';
document.getElementById('doneLink').textContent = 'Alkalmazás megtekintése';
document.getElementById('doneLink').textContent = '{{T "app_import.alkalmazas_megtekintese"}}';
}
}
} catch(e) {
@@ -305,7 +305,7 @@ function showError(msg) {
// network error, re-syncing from the server's 409 received_bytes echo; progress from bytes acked.
var fabUploadId = null;
var fabCancelled = false;
var fabNetErrText = 'A feltöltés megszakadt — ellenőrizze a kapcsolatot, majd próbálja újra.';
var fabNetErrText = '{{T "app_import.a_feltoltes_megszakadt_ellenorizze_a"}}';
function csrfRawH() {
var el = document.querySelector('meta[name="csrf-token"]');
@@ -323,7 +323,7 @@ function fabGB(b) {
function fabProgress(done, total) {
var pct = total > 0 ? Math.floor(done * 100 / total) : 0;
document.getElementById('fabUpText').textContent =
'Feltöltés: ' + pct + '% (' + fabGB(done) + ' / ' + fabGB(total) + ' GB)';
'{{T "app_import.feltoltes"}} ' + pct + '% (' + fabGB(done) + ' / ' + fabGB(total) + ' GB)';
document.getElementById('fabUpBar').style.width = pct + '%';
}
@@ -355,7 +355,7 @@ async function fabStart(file) {
if (!file) return;
document.getElementById('fabUpError').style.display = 'none';
if (!file.name.toLowerCase().endsWith('.fab')) {
fabFail('Csak .fab fájl tölthető fel.');
fabFail('{{T "app_import.csak_fab_fajl_toltheto_fel"}}');
return;
}
fabCancelled = false;
@@ -410,7 +410,7 @@ async function fabStart(file) {
var fd = await fin.json();
if (!fd.ok) { fabFail(fd.error || fabNetErrText); return; }
fabUploadId = null;
document.getElementById('fabUpText').textContent = 'Feltöltés kész: ' + fd.filename;
document.getElementById('fabUpText').textContent = '{{T "app_import.feltoltes_kesz"}} ' + fd.filename;
document.getElementById('fabUpBar').style.width = '100%';
// Refresh the bundle list through the EXISTING scan (the page render runs it).
setTimeout(function(){ window.location.reload(); }, 800);
@@ -9,13 +9,13 @@
<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">{{T "app_info.elavult"}}</span>{{end}}
{{if .Stack.Orphaned}}<span class="badge badge-orphaned">{{T "common.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">{{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 "app_info.torles"}}</button>
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Stack.Name}}')">{{T "common.torles"}}</button>
{{else}}
<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>
@@ -57,9 +57,9 @@
<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">{{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}}
{{if .Meta.Resources.NeedsHDD}}<span class="meta-badge meta-badge-warn">{{T "common.hdd_szukseges"}}</span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="meta-badge meta-badge-ok">{{T "common.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 "common.magyar_felulet"}}</span>{{end}}
</div>
{{if .Meta.IsAbandoned}}
<div class="alert alert-warning" style="margin-top:.75rem">
@@ -139,8 +139,8 @@ function appMigrate(btn,app,label){
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 "app_info.hiba"}} '+(d.error||'{{T "app_info.ismeretlen"}}')); } })
.catch(function(e){ showAlert('{{T "app_info.hiba"}} '+e); });
.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(){}); })();
@@ -188,12 +188,12 @@ function appMigrate(btn,app,label){
{{end}}
{{if .InitialCredsOperatorSet}}
<tr>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "app_info.jelszo"}}</td>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "common.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">{{T "app_info.jelszo"}}</td>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap;vertical-align:middle">{{T "common.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. -->
@@ -38,7 +38,7 @@
{{with $.SystemInfo}}
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">{{T "backups.rendszer"}}</span>
<span class="storage-label">{{T "common.rendszer_gyoker"}}</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">{{T "backups.levalasztva"}}</span></span>
<span class="storage-value">— <span class="state-text-neutral">{{T "common.levalasztva"}}</span></span>
</div>
<div class="meter-track"></div>
</div>
@@ -227,11 +227,11 @@ function triggerGuestBackup() {
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 || '{{T "backups.hiba"}}') + '</div>'; btn.disabled = false; return; }
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || '{{T "common.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">{{T "backups.hiba_2"}} ' + e + '</div>'; btn.disabled = false; });
.catch(e => { out.innerHTML = '<div class="alert alert-error">{{T "common.hiba_kettospont"}} ' + e + '</div>'; btn.disabled = false; });
});
}
@@ -285,7 +285,7 @@ function pollGuestBackup(out, btn) {
}
})
.catch(function (e) {
out.innerHTML = '<div class="alert alert-error">{{T "backups.hiba_2"}} ' + e.message + '</div>';
out.innerHTML = '<div class="alert alert-error">{{T "common.hiba_kettospont"}} ' + e.message + '</div>';
btn.disabled = false;
});
});
@@ -2,22 +2,22 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Vezérlőpult</h2>
<h2>{{T "dashboard.vezerlopult"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
<div class="stats-grid">
<a href="/stacks?filter=running" class="stat-card">
<div class="stat-value">{{.RunningCount}}</div>
<div class="stat-label">Futó alkalmazás</div>
<div class="stat-label">{{T "dashboard.futo_alkalmazas"}}</div>
</a>
<a href="/stacks?filter=stopped" class="stat-card {{if gt .StoppedCount 0}}stat-warn{{else}}stat-zero{{end}}">
<div class="stat-value">{{.StoppedCount}}</div>
<div class="stat-label">Leállítva</div>
<div class="stat-label">{{T "dashboard.leallitva"}}</div>
</a>
<a href="/stacks" class="stat-card">
<div class="stat-value">{{.TotalCount}}</div>
<div class="stat-label">Összes alkalmazás</div>
<div class="stat-label">{{T "dashboard.osszes_alkalmazas"}}</div>
</a>
</div>
@@ -26,7 +26,7 @@
<div class="system-info-items">
<div class="system-info-item meter {{usageColor .SystemInfo.MemPercent}}">
<div class="system-info-header">
<span class="system-info-label">Memória</span>
<span class="system-info-label">{{T "dashboard.memoria"}}</span>
<span class="system-info-value">{{fmtMB .SystemInfo.UsedMemMB}} / {{fmtMB .SystemInfo.TotalMemMB}} ({{printf "%.0f" .SystemInfo.MemPercent}}%)</span>
</div>
<div class="meter-track">
@@ -46,7 +46,7 @@
{{if .SystemInfo.TemperatureCelsius}}
<div class="system-info-item system-info-item-compact">
<div class="system-info-header">
<span class="system-info-label"><svg class="ico ico-sm"><use href="#i-thermometer"/></svg> Hőmérséklet</span>
<span class="system-info-label"><svg class="ico ico-sm"><use href="#i-thermometer"/></svg> {{T "dashboard.homerseklet"}}</span>
<span class="system-info-value temp-value state-text-{{tempColor .SystemInfo.TemperatureCelsius}}">{{fmtTemp .SystemInfo.TemperatureCelsius}}</span>
</div>
</div>
@@ -61,23 +61,23 @@
{{if not .SystemInfo.DiskKnown}}
<div class="system-info-item meter meter-empty">
<div class="system-info-header">
<span class="system-info-label">Rendszer (/)</span>
<span class="system-info-value">— <span class="state-text-neutral">nem ismert</span></span>
<span class="system-info-label">{{T "common.rendszer_gyoker"}}</span>
<span class="system-info-value">— <span class="state-text-neutral">{{T "dashboard.nem_ismert"}}</span></span>
</div>
<div class="meter-track"></div>
<div class="meter-flag">A tárhely mérete most nem olvasható ki.</div>
<div class="meter-flag">{{T "dashboard.a_tarhely_merete_most_nem"}}</div>
</div>
{{else}}
{{$duc := usageColor .SystemInfo.DiskPercent}}
<div class="system-info-item meter {{$duc}}">
<div class="system-info-header">
<span class="system-info-label">Rendszer (/)</span>
<span class="system-info-label">{{T "common.rendszer_gyoker"}}</span>
<span class="system-info-value">{{fmtGB .SystemInfo.DiskUsedGB}} / {{fmtGB .SystemInfo.DiskTotalGB}} ({{printf "%.0f" .SystemInfo.DiskPercent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .SystemInfo.DiskPercent}}%"></div>
</div>
{{if eq $duc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Fogyóban a hely</div>{{else if eq $duc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Kritikusan kevés hely</div>{{end}}
{{if eq $duc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.fogyoban_a_hely"}}</div>{{else if eq $duc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.kritikusan_keves_hely"}}</div>{{end}}
</div>
{{end}}
{{range .StorageBars}}
@@ -85,7 +85,7 @@
<div class="system-info-item meter meter-empty storage-disconnected">
<div class="system-info-header">
<span class="system-info-label">{{.Label}}</span>
<span class="system-info-value">— <span class="state-text-neutral">Leválasztva</span></span>
<span class="system-info-value">— <span class="state-text-neutral">{{T "common.levalasztva"}}</span></span>
</div>
<div class="meter-track"></div>
</div>
@@ -99,7 +99,7 @@
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .Percent}}%"></div>
</div>
{{if eq $uc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Fogyóban a hely</div>{{else if eq $uc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Kritikusan kevés hely</div>{{end}}
{{if eq $uc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.fogyoban_a_hely"}}</div>{{else if eq $uc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>{{T "dashboard.kritikusan_keves_hely"}}</div>{{end}}
</div>
{{end}}
{{end}}
@@ -127,7 +127,7 @@
{{end}}
<div class="disk-health-card">
<h3>Lemezek állapota</h3>
<h3>{{T "dashboard.lemezek_allapota"}}</h3>
{{if .DiskHealthRows}}
<ul class="disk-health-list">
{{range .DiskHealthRows}}
@@ -139,34 +139,34 @@
{{end}}
</ul>
{{else}}
<p class="form-hint state-text-neutral">Nincs adat</p>
<p class="form-hint state-text-neutral">{{T "dashboard.nincs_adat"}}</p>
{{end}}
</div>
{{if .BackupEnabled}}
<div class="backup-status-card">
<h3><a href="/backups" class="backup-card-link">Biztonsági mentés</a></h3>
<h3><a href="/backups" class="backup-card-link">{{T "dashboard.biztonsagi_mentes"}}</a></h3>
{{if .BackupStatus}}
<div class="backup-info-row">
<span class="backup-label">Utolsó mentés:</span>
<span class="backup-label">{{T "dashboard.utolso_mentes"}}</span>
<span class="backup-value">
{{if .BackupStatus.Success}}
<span>{{.BackupStatus.LastRun.Format "2006-01-02 15:04"}}</span>
{{else}}
<span class="backup-status-fail">Sikertelen</span>
<span class="backup-status-fail">{{T "dashboard.sikertelen"}}</span>
{{end}}
</span>
</div>
{{else}}
<div class="backup-info-row">
<span class="backup-label">Utolsó mentés:</span>
<span class="backup-value backup-status-none">Még nem futott</span>
<span class="backup-label">{{T "dashboard.utolso_mentes"}}</span>
<span class="backup-value backup-status-none">{{T "dashboard.meg_nem_futott"}}</span>
</div>
{{end}}
{{if .DBDumpStatus}}
<div class="backup-info-row">
<span class="backup-label">Adatbázisok:</span>
<span class="backup-value">{{len .DBDumpStatus.Results}} mentve</span>
<span class="backup-label">{{T "dashboard.adatbazisok"}}</span>
<span class="backup-value">{{T "dashboard.mentve"}}</span>
</div>
{{end}}
{{/* Cross-drive ("2. mentés") moved to the host agent in slice 8C — the dashboard handler no
@@ -175,7 +175,7 @@
</div>
{{end}}
<h3>Telepített alkalmazások</h3>
<h3>{{T "dashboard.telepitett_alkalmazasok"}}</h3>
<div class="stack-list">
{{range .Stacks}}
@@ -186,44 +186,44 @@
{{template "app_list_row" dict "Slug" .Meta.Slug "Name" .Meta.DisplayName "Secondary" .Meta.Description "RowClass" (printf "stack-state-%s" (stateColor .State)) "Href" (appHref .Meta.Slug)}}
{{end}}
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
{{if .Orphaned}}<span class="tag tag-warn">{{T "common.elavult"}}</span>{{end}}
{{template "meta_badge" (lifecycleBadge .Meta)}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hiányzó tárhely: {{$ms}}</span>{{end}}
{{if $.OOMKilled}}{{if index $.OOMKilled .Name}}<span class="tag tag-warn" title="Az alkalmazás egyik folyamatát a rendszer leállította, mert elfogyott a memóriája. Lehet, hogy egy feladat nem fejeződött be. Indítsd újra az alkalmazást; ha ismétlődik, jelezd az üzemeltetőnek."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Memória elfogyott</span>{{end}}{{end}}
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="Az alkalmazás környezetében a hálózati tárhely helyén üres helyi könyvtár van — az adatok nem a NAS-ra kerülnek. Jelezze az üzemeltetőnek."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely hibás — az alkalmazás nem a NAS-t látja</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
{{if and .Deployed (routeUnpublished .State)}}<span class="tag tag-warn" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL 404-et ad, pedig a konténer fut."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>URL nem elérhető</span>{{end}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="{{T "dashboard.az_alkalmazas_adattaroloja_nem_elerheto"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.hianyzo_tarhely"}}</span>{{end}}
{{if $.OOMKilled}}{{if index $.OOMKilled .Name}}<span class="tag tag-warn" title="{{T "dashboard.az_alkalmazas_egyik_folyamatat_a"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.memoria_elfogyott"}}</span>{{end}}{{end}}
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="{{T "dashboard.az_alkalmazas_kornyezeteben_a_halozati"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.halozati_tarhely_hibas_az_alkalmazas"}}</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="{{T "dashboard.a_halozati_tarhely_nas_jelenleg"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.halozati_tarhely_nem_elerheto"}}</span>{{end}}
{{if and .Deployed (routeUnpublished .State)}}<span class="tag tag-warn" title="{{T "dashboard.a_proxy_traefik_csak_egeszseges"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "dashboard.url_nem_elerheto"}}</span>{{end}}
{{if .Protected}}
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>Védett</span>
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>{{T "dashboard.vedett"}}</span>
{{/* Customer link ONLY for Linked infra (filebrowser → files.<domain>) —
cloudflared/traefik have no customer UI (guarded by render test). */}}
{{if and $im $im.Linked}}
{{$subdomain := index $.Subdomains .Name}}
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">Megnyitás <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">{{T "dashboard.megnyitas"}} <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
{{end}}
{{if isOperational .State}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="Újraindítás"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="{{T "common.ujrainditas"}}"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
{{end}}
{{else if not .Deployed}}
<a href="/stacks/{{.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>
<a href="/stacks/{{.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">{{T "dashboard.telepites"}}</a>
{{else}}
{{if isOperational .State}}
{{$subdomain := index $.Subdomains .Name}}
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">Megnyitás <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="Újraindítás"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')" title="Leállítás"><svg class="ico ico-sm"><use href="#i-square"/></svg></button>
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">{{T "dashboard.megnyitas"}} <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="{{T "common.ujrainditas"}}"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')" title="{{T "dashboard.leallitas"}}"><svg class="ico ico-sm"><use href="#i-square"/></svg></button>
{{else}}
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Name}}', 'start')" title="Indítás"><svg class="ico ico-sm"><use href="#i-play"/></svg></button>
{{if not .Orphaned}}<button class="btn btn-sm btn-danger" onclick="removeStack('{{.Name}}')">Eltávolítás</button>{{end}}
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Name}}', 'start')" title="{{T "dashboard.inditas"}}"><svg class="ico ico-sm"><use href="#i-play"/></svg></button>
{{if not .Orphaned}}<button class="btn btn-sm btn-danger" onclick="removeStack('{{.Name}}')">{{T "common.eltavolitas"}}</button>{{end}}
{{end}}
<a href="/stacks/{{.Name}}/logs" class="btn btn-sm btn-outline">Napló</a>
{{if .Orphaned}}<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Name}}')">Törlés</button>{{end}}
<a href="/stacks/{{.Name}}/logs" class="btn btn-sm btn-outline">{{T "dashboard.naplo"}}</a>
{{if .Orphaned}}<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Name}}')">{{T "common.torles"}}</button>{{end}}
{{end}}
{{template "app_list_row_end"}}
{{else}}
<div class="empty-state">
<p>Nincs elérhető alkalmazás.</p>
<p>{{T "dashboard.nincs_elerheto_alkalmazas"}}</p>
</div>
{{end}}
</div>
+161 -170
View File
@@ -3,22 +3,22 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<h2>{{.Meta.DisplayName}} — {{if .AlreadyDeployed}}Beállítások{{else}}Telepítés{{end}}</h2>
<a href="/stacks" class="btn btn-sm btn-outline">{{T "deploy.vissza"}}</a>
<h2>{{.Meta.DisplayName}} — {{if .AlreadyDeployed}}{{T "deploy.beallitasok"}}{{else}}{{T "deploy.telepites"}}{{end}}</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
{{if .AlreadyDeployed}}
{{if isOperational .Stack.State}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Stack.Name}}', 'stop')">Leállítás</button>
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Stack.Name}}', 'stop')">{{T "deploy.leallitas"}}</button>
{{else}}
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Stack.Name}}', 'start')">Indítás</button>
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Stack.Name}}', 'start')">{{T "deploy.inditas"}}</button>
{{end}}
{{if and (isOperational .Stack.State) .EffectiveSubdomain}}
<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>
<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline">{{T "deploy.megnyitas"}}</a>
{{end}}
{{end}}
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> {{T "deploy.reszletek"}}</a>
</div>
</div>
@@ -33,73 +33,72 @@
{{if .Meta.Description}}<p>{{.Meta.Description}}</p>{{end}}
<div class="metarows">
{{if .Meta.Resources.MemRequest}}<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~{{.Meta.Resources.MemRequest}}</span></span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>{{T "common.pi_kompatibilis"}}</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>{{T "common.hdd_szukseges"}}</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>{{T "common.magyar_felulet"}}</span>{{end}}
</div>
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline" style="margin-top:0.5rem">
Részletes leírás, képernyőképek
{{T "deploy.reszletes_leiras_kepernyokepek"}}
</a>
</div>
</div>
{{if .AlreadyDeployed}}
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
{{T "deploy.ez_az_alkalmazas_mar_telepitve"}}
</div>
{{if .StorageInfo}}
<div class="deploy-storage-info">
<h4>Adattárolás</h4>
<h4>{{T "deploy.adattarolas"}}</h4>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Tárhely</span>
<span class="settings-label">{{T "deploy.tarhely"}}</span>
<span class="settings-value">{{.StorageInfo.Label}} <span class="mono" style="color:var(--text-2)">({{.StorageInfo.Path}})</span></span>
</div>
{{if .StorageInfo.DataSizeHuman}}
<div class="settings-row">
<span class="settings-label">Adatméret</span>
<span class="settings-label">{{T "deploy.adatmeret"}}</span>
<span class="settings-value mono">{{.StorageInfo.DataSizeHuman}}</span>
</div>
{{end}}
{{if .StorageInfo.FreeHuman}}
<div class="settings-row">
<span class="settings-label">Szabad hely</span>
<span class="settings-value mono">{{.StorageInfo.FreeHuman}} ({{printf "%.0f" .StorageInfo.FreePercent}}% szabad)</span>
<span class="settings-label">{{T "deploy.szabad_hely"}}</span>
<span class="settings-value mono">{{T "deploy.szabad"}}</span>
</div>
{{end}}
</div>
{{if .OtherStoragePaths}}
<span class="btn btn-sm btn-outline" style="margin-top:.75rem;opacity:.45;cursor:not-allowed" title="Hamarosan">
<svg class="ico ico-sm"><use href="#i-upload"/></svg> Mozgatás másik tárolóra
<span class="btn btn-sm btn-outline" style="margin-top:.75rem;opacity:.45;cursor:not-allowed" title="{{T "deploy.hamarosan"}}">
<svg class="ico ico-sm"><use href="#i-upload"/></svg> {{T "deploy.mozgatas_masik_tarolora"}}
</span>
{{end}}
</div>
{{end}}
{{if .StaleData}}
<div class="deploy-stale-data">
<h4>Korábbi adatok</h4>
<h4>{{T "deploy.korabbi_adatok"}}</h4>
<p class="form-hint" style="margin-bottom:1rem">
Az alkalmazás adatainak másolata megtalálható egy másik tárolón is.
Ez általában áthelyezés után marad hátra.
{{T "deploy.az_alkalmazas_adatainak_masolata_megtala"}}
</p>
{{range .StaleData}}
<div class="stale-data-item">
<div class="settings-grid" style="margin-bottom:.75rem">
<div class="settings-row">
<span class="settings-label">Tárhely</span>
<span class="settings-label">{{T "deploy.tarhely"}}</span>
<span class="settings-value">{{.Label}} <span class="mono" style="color:var(--text-2)">({{.Path}})</span></span>
</div>
<div class="settings-row">
<span class="settings-label">Méret</span>
<span class="settings-label">{{T "deploy.meret"}}</span>
<span class="settings-value mono">{{.SizeHuman}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Mappák</span>
<span class="settings-label">{{T "deploy.mappak"}}</span>
<span class="settings-value mono" style="font-size:.85rem">{{range .Mounts}}{{.}}<br>{{end}}</span>
</div>
</div>
<button class="btn btn-sm btn-danger" onclick="deleteStaleData('{{$.Meta.Slug}}', '{{.Path}}', this)">
Korábbi adatok törlése
{{T "deploy.korabbi_adatok_torlese"}}
</button>
</div>
{{end}}
@@ -109,12 +108,11 @@
{{if .AlreadyDeployed}}
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<h4>{{T "deploy.biztonsagi_mentes"}}</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/{{.Stack.Name}}/backup" style="color:var(--blue)">Mentési beállítások →</a>
{{T "deploy.az_alkalmazas_adatbazisa_es_docker"}}
</span>
</div>
</div>
@@ -122,9 +120,9 @@
{{if .HasIntegrations}}
<div class="app-optional-config">
<h3>Integrációk</h3>
<h3>{{T "deploy.integraciok"}}</h3>
<p class="config-group-desc">
Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor.
{{T "deploy.mas_telepitett_alkalmazasokkal_valo_ossz"}}
</p>
{{range .Integrations}}
@@ -133,20 +131,20 @@
<strong>{{.Label}}</strong>
<p class="config-field-help" style="margin:0">{{.Description}}</p>
{{if not .TargetDeployed}}
<span class="tag tag-off" style="margin-top:.25rem"><span class="dot"></span>Nincs telepítve</span>
<span class="tag tag-off" style="margin-top:.25rem"><span class="dot"></span>{{T "deploy.nincs_telepitve"}}</span>
{{else if not .TargetRunning}}
<span class="tag tag-neutral" style="margin-top:.25rem"><span class="dot"></span>Célalkalmazás leállítva</span>
<span class="tag tag-neutral" style="margin-top:.25rem"><span class="dot"></span>{{T "deploy.celalkalmazas_leallitva"}}</span>
{{else if eq .Status "error"}}
<span class="tag tag-crit" style="margin-top:.25rem"><span class="dot"></span>Hiba</span>
<span class="tag tag-crit" style="margin-top:.25rem"><span class="dot"></span>{{T "common.hiba"}}</span>
{{else if .Enabled}}
<span class="tag tag-run" style="margin-top:.25rem"><span class="dot"></span>Aktív</span>
<span class="tag tag-run" style="margin-top:.25rem"><span class="dot"></span>{{T "deploy.aktiv"}}</span>
{{end}}
</div>
<label class="toggle">
<input type="checkbox"
{{if .Enabled}}checked{{end}}
{{if not .TargetDeployed}}disabled title="A célalkalmazás nincs telepítve"{{end}}
{{if and .TargetDeployed (not .TargetRunning)}}disabled title="A célalkalmazás nem fut"{{end}}
{{if not .TargetDeployed}}disabled title="{{T "deploy.a_celalkalmazas_nincs_telepitve"}}"{{end}}
{{if and .TargetDeployed (not .TargetRunning)}}disabled title="{{T "deploy.a_celalkalmazas_nem_fut"}}"{{end}}
onchange="toggleIntegration('{{$.Stack.Name}}', '{{.Target}}', this.checked, this)">
<span class="toggle-label"></span>
</label>
@@ -166,14 +164,14 @@
var data = await resp.json();
if (!data.ok) {
checkbox.checked = !enable;
showAlert(data.error || 'Hiba történt');
showAlert(data.error || '{{T "common.hiba_tortent"}}');
} else {
setTimeout(function(){ location.reload(); }, 500);
return;
}
} catch(err) {
checkbox.checked = !enable;
showAlert('Hálózati hiba');
showAlert('{{T "deploy.halozati_hiba"}}');
}
checkbox.disabled = false;
}
@@ -182,24 +180,24 @@
{{if .GeoGlobalEnabled}}
<div class="app-optional-config">
<h3>Földrajzi korlátozás</h3>
<h3>{{T "deploy.foldrajzi_korlatozas"}}</h3>
<p class="config-group-desc">
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
{{T "deploy.az_alkalmazas_egyeni_orszagkorlatozas_ne"}}
</p>
<label class="toggle" style="margin-bottom:1rem">
<input type="checkbox" id="app-geo-override-toggle"
{{if .GeoAppOverride}}checked{{end}}
onchange="toggleAppGeoOverride(this.checked)">
<span class="toggle-label">Egyéni országkorlátozás</span>
<span class="toggle-label">{{T "deploy.egyeni_orszagkorlatozas"}}</span>
</label>
<div id="app-geo-override-details" {{if not .GeoAppOverride}}style="display:none"{{end}}>
<div class="form-group">
<label>Engedélyezett országok ehhez az alkalmazáshoz</label>
<label>{{T "deploy.engedelyezett_orszagok_ehhez_az_alkalmaz"}}</label>
<div class="geo-country-selector">
<input type="text" id="app-geo-search" class="form-control config-input"
placeholder="Ország keresése..." autocomplete="off"
placeholder="{{T "deploy.orszag_keresese"}}" autocomplete="off"
oninput="filterAppGeoCountries(this.value)"
onfocus="showAppGeoList()"
onblur="setTimeout(function(){hideAppGeoList()},200)">
@@ -208,7 +206,7 @@
<div class="geo-selected-tags" id="app-geo-tags"></div>
</div>
<div class="config-actions">
<button class="btn btn-primary" onclick="saveAppGeoOverride()">Mentés</button>
<button class="btn btn-primary" onclick="saveAppGeoOverride()">{{T "deploy.mentes"}}</button>
<span id="app-geo-status" class="config-save-status"></span>
</div>
</div>
@@ -255,7 +253,7 @@
+ esc(c.name) + ' <small>(' + c.code + ')</small></div>';
count++;
}
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">{{T "deploy.nincs_talalat"}}</div>';
list.style.display = (count > 0 || q) ? '' : 'none';
};
@@ -296,12 +294,12 @@
})
.then(function(r){return r.json()})
.then(function(d){
status.textContent = d.ok ? (d.message || 'Mentve') : (d.error || 'Hiba');
status.textContent = d.ok ? (d.message || '{{T "deploy.mentve"}}') : (d.error || '{{T "common.hiba"}}');
status.className = 'config-save-status ' + (d.ok ? 'config-save-ok' : 'config-save-err');
setTimeout(function(){ status.textContent=''; }, 5000);
})
.catch(function(){
status.textContent = 'Hálózati hiba';
status.textContent = '{{T "deploy.halozati_hiba"}}';
status.className = 'config-save-status config-save-err';
});
};
@@ -317,7 +315,7 @@
{{if .HasOptionalConfig}}
<div class="app-optional-config">
<h3>Opcionális beállítások</h3>
<h3>{{T "deploy.opcionalis_beallitasok"}}</h3>
{{range .OptionalConfig}}
<div class="config-group">
<h4>{{.Group}}</h4>
@@ -328,7 +326,7 @@
<div class="config-field">
<label for="opt-{{.EnvVar}}">{{.Label}}</label>
{{if .HelpText}}<p class="config-field-help">{{.HelpText}}</p>{{end}}
{{if .HelpURL}}<p class="config-field-help"><a href="{{.HelpURL}}" target="_blank">Regisztrációs útmutató ↗</a></p>{{end}}
{{if .HelpURL}}<p class="config-field-help"><a href="{{.HelpURL}}" target="_blank">{{T "deploy.regisztracios_utmutato"}}</a></p>{{end}}
<input type="{{if eq .Type "secret_input"}}password{{else}}text{{end}}"
id="opt-{{.EnvVar}}"
name="{{.EnvVar}}"
@@ -344,7 +342,7 @@
<div class="config-actions">
<button class="btn btn-primary" id="save-optional-config" onclick="saveOptionalConfig('{{.Stack.Name}}')">
Mentés
{{T "deploy.mentes"}}
</button>
<span id="config-save-status" class="config-save-status"></span>
</div>
@@ -362,7 +360,7 @@
});
btn.disabled = true;
btn.textContent = 'Mentés...';
btn.textContent = '{{T "deploy.mentes_2"}}';
status.textContent = '';
status.className = 'config-save-status';
@@ -375,19 +373,19 @@
var data = await resp.json();
if (data.ok) {
status.textContent = (data.message || 'Mentve');
status.textContent = (data.message || '{{T "deploy.mentve"}}');
status.className = 'config-save-status config-save-ok';
} else {
status.textContent = (data.error || 'Hiba');
status.textContent = (data.error || '{{T "common.hiba"}}');
status.className = 'config-save-status config-save-err';
}
} catch(err) {
status.textContent = 'Hálózati hiba';
status.textContent = '{{T "deploy.halozati_hiba"}}';
status.className = 'config-save-status config-save-err';
}
btn.disabled = false;
btn.textContent = 'Mentés';
btn.textContent = '{{T "deploy.mentes"}}';
setTimeout(function() { status.textContent = ''; }, 5000);
}
@@ -396,26 +394,24 @@
{{if .AppEmailSupported}}
<div class="app-optional-config">
<h3>Email-küldés</h3>
<h3>{{T "deploy.email_kuldes"}}</h3>
<p class="config-group-desc">
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
külön szolgáltató beállítása nélkül. A feladó címe: <strong>{{.AppEmailFromAddress}}</strong>.
{{T "deploy.ez_az_alkalmazas_tud_emailt"}}
</p>
{{if not .AppEmailGlobalOn}}
<p class="alert alert-warning" style="margin-bottom:1rem">
Az alkalmazás-email jelenleg ki van kapcsolva globálisan. Kapcsold be a
<a href="/settings">Beállítások</a> oldalon, hogy itt is működjön.
{{T "deploy.az_alkalmazas_email_jelenleg_ki"}}
</p>
{{end}}
<form method="POST" action="/stacks/{{.Stack.Name}}/app-email">
{{$.CSRFField}}
<label style="display:flex;align-items:center;gap:.5rem;margin-bottom:1rem">
<input type="checkbox" name="app_email_enabled" value="on" {{if .AppEmailAppOn}}checked{{end}}>
Email-küldés engedélyezése ennél az alkalmazásnál
{{T "deploy.email_kuldes_engedelyezese_ennel_az"}}
</label>
<div class="config-actions">
<button class="btn btn-primary" type="submit">Mentés</button>
<span class="config-group-desc" style="margin-left:.5rem">A mentés újraindítja az alkalmazást.</span>
<button class="btn btn-primary" type="submit">{{T "deploy.mentes"}}</button>
<span class="config-group-desc" style="margin-left:.5rem">{{T "deploy.a_mentes_ujrainditja_az_alkalmazast"}}</span>
</div>
</form>
</div>
@@ -427,25 +423,24 @@
<div class="memory-summary{{if .Blocked}} memory-blocked{{end}}">
{{if .Blocked}}
<div class="alert alert-error" style="margin-bottom:0">
Nincs elég memória! Foglalás telepítés után: {{.AfterMB}} MB / {{.UsableMB}} MB
{{T "deploy.nincs_eleg_memoria_foglalas_telepites"}}
</div>
{{else}}
<div class="memory-summary-header">
<span class="memory-summary-label">Memória</span>
<span class="memory-summary-label">{{T "deploy.memoria"}}</span>
<span class="memory-summary-value">{{.AfterMB}} MB / {{.UsableMB}} MB ({{.Percent}}%)</span>
</div>
<div class="meter-track meter-track-stacked">
<div class="memory-bar-segment memory-bar-committed" style="width:{{.UsedPercent}}%" title="Jelenlegi használat: {{.UsedMB}} MB"></div>
<div class="memory-bar-segment memory-bar-committed" style="width:{{.UsedPercent}}%" title="{{T "deploy.jelenlegi_hasznalat_mb"}}"></div>
<div class="memory-bar-segment memory-bar-new" style="width:{{subtract .Percent .UsedPercent}}%" title="{{$.Meta.DisplayName}}: +{{.NewRequestMB}} MB"></div>
</div>
<div class="memory-bar-legend">
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-committed"></span>Jelenlegi használat ({{.UsedMB}} MB)</span>
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-committed"></span>{{T "deploy.jelenlegi_hasznalat_mb_2"}}</span>
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-new"></span>{{$.Meta.DisplayName}} (+{{.NewRequestMB}} MB)</span>
</div>
{{if .OvercommitWarn}}
<div class="alert alert-warning" style="margin-top:0.5rem;margin-bottom:0">
Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát.
Normál használat mellett ez nem okoz problémát.
{{T "deploy.az_alkalmazasok_csucsterhelese_meghaladh"}}
</div>
{{end}}
{{end}}
@@ -461,10 +456,10 @@
{{if and (not .AlreadyDeployed) .RestoreHasRecord}}
<div class="alert alert-info" style="margin-bottom:1rem">
<svg class="ico ico-sm"><use href="#i-info"/></svg>
Ennek az alkalmazásnak van korábbi mentése, ezért az alábbi mezőket a saját mentése alapján töltöttük ki.
{{if .RestoreRecordedAddress}}Korábbi webcím: <strong>{{.RestoreRecordedAddress}}</strong>.{{end}}
{{if .RestoreRecordedDrive}}Az adatai itt voltak: <strong>{{.RestoreRecordedDrive}}</strong>.{{end}}
Ha most máshová szeretnéd telepíteni, nyugodtan átírhatod — a visszaállítás előtt jelezni fogjuk az eltérést.
{{T "deploy.ennek_az_alkalmazasnak_van_korabbi"}}
{{if .RestoreRecordedAddress}}{{T "deploy.korabbi_webcim"}}{{end}}
{{if .RestoreRecordedDrive}}{{T "deploy.az_adatai_itt_voltak"}}{{end}}
{{T "deploy.ha_most_mashova_szeretned_telepiteni"}}
</div>
{{end}}
{{/* R-351 / Part 1 — WHERE THE DATA WILL LIVE, before the button is pressed. Measured
@@ -474,27 +469,24 @@
{{if not .AlreadyDeployed}}
<div class="form-hint" style="margin-bottom:1rem">
{{if .RestoreRecordedDeclaresPath}}
Az alkalmazás adatai az alább kiválasztott adatmeghajtóra kerülnek.
{{T "deploy.az_alkalmazas_adatai_az_alabb"}}
{{else}}
<strong>Hol lesznek az adatok:</strong> ennél az alkalmazásnál nincs külön adatmeghajtó-választás,
ezért az adatai a rendszermeghajtóra kerülnek (<code>{{.SystemDataPath}}</code>). A mentései így is elkészülnek.
{{T "deploy.hol_lesznek_az_adatok_ennel"}}
{{end}}
</div>
{{end}}
{{if .DockerBelowReserve}}
<div class="alert alert-warning" style="margin-bottom:1rem">
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Nincs elég szabad tárhely a telepítéshez. Jelenleg {{.DockerFreeHuman}} szabad, és a rendszer
{{.DockerReserveHuman}} tartalékot tart fenn az alapszolgáltatások (vezérlő, proxy) védelmében.
A telepítés ezért átmenetileg le van tiltva — szabadítson fel helyet, vagy bővítse a tárhelyet.
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "deploy.nincs_eleg_szabad_tarhely_a"}}
</div>
{{end}}
{{if .AutoFields}}
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
<h4>{{T "deploy.automatikusan_generalt_ertekek"}}</h4>
{{if .AlreadyDeployed}}
<p class="form-section-desc">Ezek az értékek automatikusan jöttek létre a telepítéskor.</p>
<p class="form-section-desc">{{T "deploy.ezek_az_ertekek_automatikusan_jottek"}}</p>
{{else}}
<p class="form-section-desc">Ezek az értékek a telepítéssel együtt mentésre kerülnek. Jegyezze fel a szükséges jelszavakat!</p>
<p class="form-section-desc">{{T "deploy.ezek_az_ertekek_a_telepitessel"}}</p>
{{end}}
{{$autoValues := .AutoFieldValues}}
{{$stackName := .Stack.Name}}
@@ -502,7 +494,7 @@
{{range .AutoFields}}
{{$val := index $autoValues .EnvVar}}
<div class="form-group form-group-auto">
<label>{{.Label}} {{if eq .Type "secret"}}<span class="auto-generated-badge"><svg class="ico ico-sm"><use href="#i-check"/></svg> Automatikusan generálva</span>{{end}}</label>
<label>{{.Label}} {{if eq .Type "secret"}}<span class="auto-generated-badge"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "deploy.automatikusan_generalva"}}</span>{{end}}</label>
{{if $val}}
{{if eq .Type "secret"}}
{{if $isDeployed}}
@@ -512,12 +504,12 @@
submits, so the saved value is the one the customer was shown (README §318). -->
<div class="input-with-button">
<input type="password" id="auto-field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('{{$stackName}}','{{.EnvVar}}', this)">Megjelenítés</button>
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('{{$stackName}}','{{.EnvVar}}', this)">{{T "deploy.megjelenites"}}</button>
</div>
{{else}}
<div class="input-with-button">
<input type="password" id="auto-field-{{.EnvVar}}" class="form-control" value="{{$val}}" readonly>
<button type="button" class="btn btn-sm btn-outline" onclick="toggleAutoField('auto-field-{{.EnvVar}}', this)">Megjelenítés</button>
<button type="button" class="btn btn-sm btn-outline" onclick="toggleAutoField('auto-field-{{.EnvVar}}', this)">{{T "deploy.megjelenites"}}</button>
</div>
{{end}}
{{else}}
@@ -534,13 +526,13 @@
{{if .UserFields}}
<div class="form-section">
<h4>Beállítások</h4>
<h4>{{T "deploy.beallitasok"}}</h4>
{{range .UserFields}}
<div class="form-group">
<label for="field-{{.EnvVar}}">
{{.Label}}
{{if or .Required (eq .Type "password")}}<span class="required">*</span>{{end}}
{{if .LockedAfterDeploy}}<span class="locked-hint">telepítés után nem módosítható</span>{{end}}
{{if .LockedAfterDeploy}}<span class="locked-hint">{{T "deploy.telepites_utan_nem_modosithato"}}</span>{{end}}
</label>
{{if eq .Type "subdomain"}}
@@ -550,7 +542,7 @@
{{/* R-351: a reinstall offers the address the customer's own backup recorded,
ahead of the catalog default. Editable — a domain can legitimately change. */}}
value="{{if and $.AlreadyDeployed $.DeployedFieldValues}}{{index $.DeployedFieldValues .EnvVar}}{{else if index $.RestoreFieldValues .EnvVar}}{{index $.RestoreFieldValues .EnvVar}}{{else}}{{.Default}}{{end}}"
placeholder="aldomain"
placeholder="{{T "deploy.aldomain_placeholder"}}"
pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?"
required
{{if $.AlreadyDeployed}}disabled{{end}}
@@ -558,7 +550,7 @@
<span class="subdomain-suffix">.{{$.Domain}}</span>
</div>
{{if not $.AlreadyDeployed}}
<span class="form-hint">Az aldomain telepítés után nem módosítható. Megváltoztatáshoz az alkalmazás eltávolítása és újratelepítése szükséges (minden adat törlődik).</span>
<span class="form-hint">{{T "deploy.az_aldomain_telepites_utan_nem"}}</span>
{{end}}
{{else if eq .Type "select"}}
<select id="field-{{.EnvVar}}" name="{{.EnvVar}}" class="form-control"
@@ -577,18 +569,18 @@
{{if $.AlreadyDeployed}}disabled{{end}}>
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
onclick="togglePasswordField('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}', this)"
title="Megjelenítés">&#128065;</button>
title="{{T "deploy.megjelenites"}}">&#128065;</button>
{{if not $.AlreadyDeployed}}
<button type="button" class="btn btn-sm btn-outline"
onclick="generatePassword('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}')">Generálás</button>
onclick="generatePassword('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}')">{{T "deploy.generalas"}}</button>
{{end}}
</div>
{{if $.AlreadyDeployed}}
<span class="form-hint">Telepítéskor beállított kezdeti jelszó — ha az alkalmazásban megváltoztattad, az itt nem frissül.</span>
<span class="form-hint">{{T "deploy.telepiteskor_beallitott_kezdeti_jelszo_h"}}</span>
{{else}}
<div class="input-with-button" style="margin-top:.25rem">
<input type="password" id="field-confirm-{{.EnvVar}}"
class="form-control" placeholder="Jelszó megerősítése"
class="form-control" placeholder="{{T "deploy.jelszo_megerositese"}}"
data-confirm-for="field-{{.EnvVar}}">
</div>
{{end}}
@@ -596,7 +588,7 @@
<label class="toggle">
<input type="checkbox" id="field-{{.EnvVar}}" name="{{.EnvVar}}" value="true"
{{if $.AlreadyDeployed}}disabled{{end}}>
<span class="toggle-label">Igen</span>
<span class="toggle-label">{{T "deploy.igen"}}</span>
</label>
{{else if eq .Type "path"}}
{{if $.StoragePaths}}
@@ -611,19 +603,18 @@
restoring into a different drive is the mismatch the restore then has
to stop and name. The customer can still pick another. */}}
{{if $.AlreadyDeployed}}{{if eq .Path $.CurrentHDDPath}}selected{{end}}{{else if $.RestorePrefillHDDPath}}{{if and (eq .Path $.RestorePrefillHDDPath) (not .NotAllowed)}}selected{{end}}{{else if and .IsDefault (not .NotAllowed)}}selected{{end}}>
{{.Label}} — {{.FreeHuman}} szabad{{if .NotAllowed}} ({{.NotAllowedNote}}){{else if .IsDefault}} (alapértelmezett){{end}}
{{T "deploy.szabad_2"}}{{if .NotAllowed}} ({{.NotAllowedNote}}){{else if .IsDefault}} {{T "deploy.alapertelmezett"}}{{end}}
</option>
{{end}}
{{if $.CurrentHDDPathMissing}}
<option value="{{$.CurrentHDDPath}}" selected disabled>{{$.CurrentHDDPath}} (nem elérhető)</option>
<option value="{{$.CurrentHDDPath}}" selected disabled>{{T "deploy.nem_elerheto"}}</option>
{{end}}
</select>
<div id="storage-space-warn" class="form-hint" style="color:var(--warn);display:none">
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A kiválasztott tárhely majdnem megtelt.
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "deploy.a_kivalasztott_tarhely_majdnem_megtelt"}}
</div>
<div class="form-hint" style="margin-top:.4rem;opacity:.8">
<svg class="ico ico-sm"><use href="#i-info"/></svg> A kiválasztott meghajtón az alkalmazás <strong>fájljai</strong> (média, dokumentumok) tárolódnak.
Az <strong>adatbázis a gyors belső SSD-n</strong> fut — és az alkalmazással együtt készül róla biztonsági mentés.
<svg class="ico ico-sm"><use href="#i-info"/></svg> {{T "deploy.a_kivalasztott_meghajton_az_alkalmazas"}}
</div>
{{else}}
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
@@ -631,7 +622,7 @@
placeholder="{{.Placeholder}}"
{{if .Required}}required{{end}}
{{if $.AlreadyDeployed}}disabled{{end}}>
<span class="form-hint" style="color:var(--warn)">Nincs regisztrált adattároló — adja meg kézzel az útvonalat</span>
<span class="form-hint" style="color:var(--warn)">{{T "deploy.nincs_regisztralt_adattarolo_adja_meg"}}</span>
{{end}}
{{else}}
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
@@ -651,26 +642,26 @@
{{if not .AlreadyDeployed}}
<div class="deploy-actions">
<button type="submit" class="btn btn-primary btn-lg"{{if .DockerBelowReserve}} disabled title="Nincs elég szabad tárhely"{{else if and .MemoryInfo (index .MemoryInfo "Blocked")}} disabled title="Nincs elég memória"{{end}}>Telepítés indítása</button>
<a href="/stacks" class="btn btn-outline">Mégsem</a>
<button type="submit" class="btn btn-primary btn-lg"{{if .DockerBelowReserve}} disabled title="{{T "deploy.nincs_eleg_szabad_tarhely"}}"{{else if and .MemoryInfo (index .MemoryInfo "Blocked")}} disabled title="{{T "deploy.nincs_eleg_memoria"}}"{{end}}>{{T "deploy.telepites_inditasa"}}</button>
<a href="/stacks" class="btn btn-outline">{{T "common.megsem"}}</a>
</div>
{{end}}
</form>
<div id="deploy-progress" class="deploy-progress" style="display:none">
<h3>Telepítés folyamatban...</h3>
<h3>{{T "deploy.telepites_folyamatban"}}</h3>
<div class="deploy-steps">
<div class="deploy-step active" id="step-config">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konfiguráció mentése...</span>
<span class="step-text">{{T "deploy.konfiguracio_mentese"}}</span>
</div>
<div class="deploy-step" id="step-containers">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konténer(ek) indítása...</span>
<span class="step-text">{{T "deploy.kontener_ek_inditasa"}}</span>
</div>
<div class="deploy-step" id="step-health">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Alkalmazás inicializálása...</span>
<span class="step-text">{{T "deploy.alkalmazas_inicializalasa"}}</span>
</div>
</div>
<div id="deploy-warning" class="alert alert-warning" style="display:none"></div>
@@ -701,13 +692,13 @@ function buildPostDeployCard(stackName) {
if (subdomain && postDeployInfo.domain) {
var appURL = 'https://' + subdomain + '.' + postDeployInfo.domain;
html += '<div style="text-align:center;margin:1.5rem 0">' +
'<a href="' + appURL + '" target="_blank" class="btn btn-primary btn-lg">Alkalmazás megnyitása ↗</a>' +
'<a href="' + appURL + '" target="_blank" class="btn btn-primary btn-lg">{{T "deploy.alkalmazas_megnyitasa"}}</a>' +
'</div>';
}
// First steps — with dynamic credential hint
if (postDeployInfo.firstSteps && postDeployInfo.firstSteps.length > 0) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Első lépések</h4><ol class="app-info-list">';
html += '<div class="app-info-card" style="margin-top:1rem"><h4>{{T "deploy.elso_lepesek"}}</h4><ol class="app-info-list">';
for (var i = 0; i < postDeployInfo.firstSteps.length; i++) {
var step = postDeployInfo.firstSteps[i].replace(/DOMAIN/g, postDeployInfo.domain);
html += '<li>' + step + '</li>';
@@ -742,8 +733,8 @@ function buildPostDeployCard(stackName) {
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;display:flex;align-items:center;gap:.5rem">' +
'<code id="' + credId + '" style="font-family:var(--font-data);letter-spacing:1px">••••••••••••</code>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="togglePostDeployCred(this,\'' + credId + '\',\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Megjelenítés</button>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="copyPostDeployCred(this,\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Másolás</button>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="togglePostDeployCred(this,\'' + credId + '\',\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">{{T "deploy.megjelenites"}}</button>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="copyPostDeployCred(this,\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">{{T "deploy.masolas"}}</button>' +
'</td></tr>';
} else {
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
@@ -752,23 +743,23 @@ function buildPostDeployCard(stackName) {
}
}
if (credRows) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
html += '<div class="app-info-card" style="margin-top:1rem"><h4>{{T "deploy.bejelentkezes"}}</h4>' +
'<table style="margin:.5rem 0">' + credRows + '</table></div>';
} else if (postDeployInfo.defaultCreds) {
var creds = postDeployInfo.defaultCreds.replace(/DOMAIN/g, postDeployInfo.domain);
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
html += '<div class="app-info-card" style="margin-top:1rem"><h4>{{T "deploy.bejelentkezes"}}</h4>' +
'<p class="app-info-creds">' + creds + '</p></div>';
}
// Docs link
if (postDeployInfo.docsURL) {
html += '<div style="margin-top:1rem"><a href="' + postDeployInfo.docsURL + '" target="_blank" class="btn btn-sm btn-outline">Dokumentáció ↗</a></div>';
html += '<div style="margin-top:1rem"><a href="' + postDeployInfo.docsURL + '" target="_blank" class="btn btn-sm btn-outline">{{T "deploy.dokumentacio"}}</a></div>';
}
// Action buttons
html += '<div style="display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap">' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">{{T "deploy.beallitasok_es_jelszavak_megtekintese"}}</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">{{T "deploy.alkalmazasok"}}</a>' +
'</div>';
return html;
@@ -796,7 +787,7 @@ function revealAutoField(stackName, envVar, btn) {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
btn.textContent = '{{T "deploy.megjelenites"}}';
return;
}
btn.disabled = true;
@@ -807,18 +798,18 @@ function revealAutoField(stackName, envVar, btn) {
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
if (!j.ok) { showAlert(j.error || '{{T "deploy.a_lekeres_nem_sikerult"}}'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
btn.textContent = '{{T "deploy.elrejtes"}}';
}).catch(function () { btn.disabled = false; showAlert('{{T "deploy.a_lekeres_nem_sikerult"}}'); });
}
function toggleAutoField(fieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
el.type = el.type === 'password' ? 'text' : 'password';
btn.textContent = el.type === 'password' ? 'Megjelenítés' : 'Elrejtés';
btn.textContent = el.type === 'password' ? '{{T "deploy.megjelenites"}}' : '{{T "deploy.elrejtes"}}';
}
function generatePassword(fieldId, confirmFieldId) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@@ -841,7 +832,7 @@ function togglePasswordField(fieldId, confirmFieldId, btn) {
el.type = newType;
var ce = document.getElementById(confirmFieldId);
if (ce) ce.type = newType;
btn.title = newType === 'password' ? 'Megjelenítés' : 'Elrejtés';
btn.title = newType === 'password' ? '{{T "deploy.megjelenites"}}' : '{{T "deploy.elrejtes"}}';
}
function togglePostDeployCred(btn, credId, val) {
@@ -849,17 +840,17 @@ function togglePostDeployCred(btn, credId, val) {
if (!el) return;
if (el.textContent === val) {
el.textContent = '••••••••••••';
btn.textContent = 'Megjelenítés';
btn.textContent = '{{T "deploy.megjelenites"}}';
} else {
el.textContent = val;
btn.textContent = 'Elrejtés';
btn.textContent = '{{T "deploy.elrejtes"}}';
}
}
function copyPostDeployCred(btn, val) {
if (navigator.clipboard) {
navigator.clipboard.writeText(val).then(function() {
var orig = btn.textContent;
btn.textContent = 'Másolva!';
btn.textContent = '{{T "deploy.masolva"}}';
setTimeout(function() { btn.textContent = orig; }, 1500);
});
}
@@ -867,8 +858,8 @@ function copyPostDeployCred(btn, val) {
function deleteStaleData(stackName, stalePath, btn) {
// Two-step inline confirm (the irreversible delete keeps its double acknowledgement).
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
felhomConfirm(btn, '{{T "deploy.biztosan_torolni_szeretned_a_korabbi"}} ' + stalePath + ' {{T "deploy.figyelem_a_muvelet_visszavonhatatlan_elo"}}', function () {
felhomConfirm(btn, '{{T "deploy.utolso_figyelmeztetes_a_torles_visszavon"}}', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
@@ -876,7 +867,7 @@ function deleteStaleData(stackName, stalePath, btn) {
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
btn.textContent = '{{T "deploy.torles_folyamatban"}}';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
@@ -886,14 +877,14 @@ function deleteStaleDataRun(stackName, stalePath, btn) {
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
showAlert('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "deploy.ismeretlen_hiba"}}'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
btn.textContent = '{{T "deploy.korabbi_adatok_torlese"}}';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
var msg = '{{T "deploy.korabbi_adatok_torolve_n_nfelszabaditott"}} ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
msg += '{{T "deploy.n_nnehany_hiba_tortent_n"}}' + data.errors.join('\n');
}
showAlert(msg);
// Remove the stale data card from DOM
@@ -906,9 +897,9 @@ function deleteStaleDataRun(stackName, stalePath, btn) {
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
showAlert('{{T "deploy.halozati_hiba_2"}} ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
btn.textContent = '{{T "deploy.korabbi_adatok_torlese"}}';
});
}
@@ -920,7 +911,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
for (const pf of passwordFields) {
if (!pf.disabled && pf.value.trim() === '') {
const label = pf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label + '\nHasználja a Generálás gombot vagy írjon be egy jelszót.');
showAlert('{{T "deploy.kotelezo_mezo"}} ' + label + '{{T "deploy.nhasznalja_a_generalas_gombot_vagy"}}');
pf.focus();
return;
}
@@ -932,7 +923,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
const mainEl = document.getElementById(ci.getAttribute('data-confirm-for'));
if (mainEl && !mainEl.disabled && ci.value !== mainEl.value) {
const label = mainEl.closest('.form-group').querySelector('label').textContent.trim();
showAlert('A két jelszó nem egyezik: ' + label);
showAlert('{{T "deploy.a_ket_jelszo_nem_egyezik"}} ' + label);
ci.focus();
return;
}
@@ -943,7 +934,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
if (subdomainField && !subdomainField.disabled) {
const sd = subdomainField.value.trim().toLowerCase();
if (!sd || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sd)) {
showAlert('Az aldomain csak kisbetűket, számokat és kötőjelet tartalmazhat, és nem kezdődhet/végződhet kötőjellel.');
showAlert('{{T "deploy.az_aldomain_csak_kisbetuket_szamokat"}}');
subdomainField.focus();
return;
}
@@ -954,7 +945,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
for (const rf of requiredFields) {
if (!rf.disabled && rf.value.trim() === '') {
const label = rf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label);
showAlert('{{T "deploy.kotelezo_mezo"}} ' + label);
rf.focus();
return;
}
@@ -962,7 +953,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
const btn = e.target.querySelector('[type=submit]');
btn.disabled = true;
btn.textContent = 'Telepítés folyamatban...';
btn.textContent = '{{T "deploy.telepites_folyamatban"}}';
const values = {};
const inputs = e.target.querySelectorAll('input, select');
@@ -1005,8 +996,8 @@ document.getElementById('deploy-form').addEventListener('submit', async function
});
var data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + data.error);
btn.textContent = 'Telepítés indítása';
showAlert('{{T "common.hiba_kettospont"}} ' + data.error);
btn.textContent = '{{T "deploy.telepites_inditasa"}}';
btn.disabled = false;
return;
}
@@ -1014,8 +1005,8 @@ document.getElementById('deploy-form').addEventListener('submit', async function
// Deploy API returned success — switch to progress view
formEl.style.display = 'none';
progressEl.style.display = 'block';
setStep(stepConfig, 'done', 'Konfiguráció mentve');
setStep(stepContainers, 'active', 'Konténer(ek) indítása...');
setStep(stepConfig, 'done', '{{T "deploy.konfiguracio_mentve"}}');
setStep(stepContainers, 'active', '{{T "deploy.kontener_ek_inditasa"}}');
if (data.data && data.data.warning) {
warningEl.textContent = data.data.warning;
@@ -1027,13 +1018,13 @@ document.getElementById('deploy-form').addEventListener('submit', async function
var pollTimeout = 120000;
var pollTimer = setInterval(async function() {
var elapsed = Math.round((Date.now() - startTime) / 1000);
elapsedEl.textContent = elapsed + ' másodperce...';
elapsedEl.textContent = elapsed + ' {{T "deploy.masodperce"}}';
if (Date.now() - startTime > pollTimeout) {
clearInterval(pollTimer);
setStep(stepHealth, 'warn', 'Időtúllépés — az alkalmazás még indulhat');
setStep(stepHealth, 'warn', '{{T "deploy.idotullepes_az_alkalmazas_meg_indulhat"}}');
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'A telepítés időtúllépésbe futott. Az alkalmazás még indulhat.' +
'{{T "deploy.a_telepites_idotullepesbe_futott_az"}}' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
return;
@@ -1049,56 +1040,56 @@ document.getElementById('deploy-form').addEventListener('submit', async function
if (deployError) {
// Async compose-up failed
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'Telepítés sikertelen');
setStep(stepContainers, 'error', '{{T "deploy.telepites_sikertelen"}}');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
progressEl.querySelector('h3').textContent = '{{T "deploy.telepites_sikertelen"}}';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A telepítés nem sikerült: ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
'{{T "deploy.a_telepites_nem_sikerult"}} ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">{{T "deploy.naplok_megtekintese"}}</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">{{T "deploy.alkalmazasok_2"}}</a>';
resultEl.style.display = 'block';
} else if (state === 'deploying') {
// Compose up in progress (pulling images, creating containers)
setStep(stepContainers, 'active', 'Képek letöltése, konténerek indítása...');
setStep(stepContainers, 'active', '{{T "deploy.kepek_letoltese_kontenerek_inditasa"}}');
} else if (state === 'running') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'done', 'Alkalmazás kész!');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
setStep(stepContainers, 'done', '{{T "deploy.kontenerek_elindultak"}}');
setStep(stepHealth, 'done', '{{T "deploy.alkalmazas_kesz"}}');
progressEl.querySelector('h3').textContent = '{{T "deploy.telepites_sikeres"}}';
resultEl.innerHTML = '<div class="alert alert-info" style="margin-top:1rem">' +
'Az alkalmazás sikeresen telepítve és fut!' +
'{{T "deploy.az_alkalmazas_sikeresen_telepitve_es"}}' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'starting') {
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'active', 'Alkalmazás inicializálása...');
setStep(stepContainers, 'done', '{{T "deploy.kontenerek_elindultak"}}');
setStep(stepHealth, 'active', '{{T "deploy.alkalmazas_inicializalasa"}}');
} else if (state === 'unhealthy') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'warn', 'Állapotjelző: még inicializál');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
setStep(stepContainers, 'done', '{{T "deploy.kontenerek_elindultak"}}');
setStep(stepHealth, 'warn', '{{T "deploy.allapotjelzo_meg_inicializal"}}');
progressEl.querySelector('h3').textContent = '{{T "deploy.telepites_sikeres"}}';
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'Az alkalmazás elindult, de még inicializálódik. ' +
'Ez normális az első percekben — az alkalmazás már elérhető lehet.' +
'{{T "deploy.az_alkalmazas_elindult_de_meg"}} ' +
'{{T "deploy.ez_normalis_az_elso_percekben"}}' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'exited' || state === 'stopped') {
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'A konténer leállt');
setStep(stepContainers, 'error', '{{T "deploy.a_kontener_leallt"}}');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
progressEl.querySelector('h3').textContent = '{{T "deploy.telepites_sikertelen"}}';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A konténer leállt. Ellenőrizze a naplókat.' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
'{{T "deploy.a_kontener_leallt_ellenorizze_a"}}' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">{{T "deploy.naplok_megtekintese"}}</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">{{T "deploy.alkalmazasok_2"}}</a>';
resultEl.style.display = 'block';
}
} catch(pollErr) {}
}, 3000);
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = 'Telepítés indítása';
showAlert('{{T "deploy.halozati_hiba_2"}} ' + err.message);
btn.textContent = '{{T "deploy.telepites_inditasa"}}';
btn.disabled = false;
}
});
@@ -68,7 +68,7 @@
</div>
<div class="share-field">
{{if .SharePasswordSet}}
<label class="share-label">{{T "launcher.jelszo"}}</label>
<label class="share-label">{{T "common.jelszo"}}</label>
<p class="share-note">{{T "launcher.a_megosztas_jelszoval_vedett"}}</p>
<form method="POST" action="/launcher/share/password">
{{.CSRFField}}
@@ -101,14 +101,14 @@
<form method="POST" action="/launcher/share/enable">
{{.CSRFField}}
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="closeShareModal()">{{T "launcher.bezaras"}}</button>
<button type="button" class="btn btn-outline" onclick="closeShareModal()">{{T "common.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()">{{T "launcher.bezaras"}}</button>
<button type="button" class="btn btn-outline" onclick="closeShareModal()">{{T "common.bezaras"}}</button>
</div>
{{end}}
</div>
+18 -18
View File
@@ -265,7 +265,7 @@
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || '{{T "layout.sablonok_frissitve"}}') : ('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
toast.textContent = data.ok ? (data.message || '{{T "layout.sablonok_frissitve"}}') : ('{{T "common.hiba_kettospont"}} ' + (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);
@@ -298,7 +298,7 @@
});
const data = await resp.json();
if (!data.ok) {
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
showAlert('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
@@ -358,13 +358,13 @@
'<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()">{{T "layout.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">{{T "layout.torles"}}</button>' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">{{T "common.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">{{T "common.torles"}}</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<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>';
'<h3>{{T "common.hiba"}}</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 "common.bezaras"}}</button></div>';
}
}
function closeDeleteModal() {
@@ -401,16 +401,16 @@
'<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\'">{{T "layout.bezaras"}}</button></div>';
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">{{T "common.bezaras"}}</button></div>';
} else {
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
showAlert('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.disabled = false;
btn.textContent = '{{T "layout.torles"}}';
btn.textContent = '{{T "common.torles"}}';
}
} catch (err) {
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.disabled = false;
btn.textContent = '{{T "layout.torles"}}';
btn.textContent = '{{T "common.torles"}}';
}
}
async function removeStack(name) {
@@ -471,13 +471,13 @@
'<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()">{{T "layout.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">{{T "layout.eltavolitas"}}</button>' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">{{T "common.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">{{T "common.eltavolitas"}}</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<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>';
'<h3>{{T "common.hiba"}}</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 "common.bezaras"}}</button></div>';
}
}
function closeRemoveModal() {
@@ -522,16 +522,16 @@
'<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\'">{{T "layout.bezaras"}}</button></div>';
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">{{T "common.bezaras"}}</button></div>';
} else {
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
showAlert('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.disabled = false;
btn.textContent = '{{T "layout.eltavolitas"}}';
btn.textContent = '{{T "common.eltavolitas"}}';
}
} catch (err) {
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.disabled = false;
btn.textContent = '{{T "layout.eltavolitas"}}';
btn.textContent = '{{T "common.eltavolitas"}}';
}
}
</script>
+9 -9
View File
@@ -1,18 +1,18 @@
{{define "logs"}}
{{template "layout_start" .}}
<div class="page-header">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<h2>{{.Stack.Meta.DisplayName}} — Naplók</h2>
<a href="/stacks" class="btn btn-sm btn-outline">{{T "logs.vissza"}}</a>
<h2>{{T "logs.naplok"}}</h2>
</div>
<div class="logs-container" id="logs-container">
<pre class="logs-output" id="logs-output">{{.Logs}}</pre>
</div>
<div class="logs-actions">
<span class="logs-live-indicator" id="live-indicator">
<span class="logs-live-dot"></span> Élő
<span class="logs-live-dot"></span> {{T "logs.elo"}}
</span>
<button class="btn btn-outline btn-sm" id="logs-toggle" onclick="toggleLive()">Szüneteltetés</button>
<button class="btn btn-outline btn-sm" onclick="fetchLogs()">Frissítés</button>
<button class="btn btn-outline btn-sm" id="logs-toggle" onclick="toggleLive()">{{T "logs.szuneteltetes"}}</button>
<button class="btn btn-outline btn-sm" onclick="fetchLogs()">{{T "logs.frissites"}}</button>
</div>
<script>
(function() {
@@ -53,13 +53,13 @@
if (live) {
startPolling();
indicator.className = 'logs-live-indicator';
indicator.innerHTML = '<span class="logs-live-dot"></span> Élő';
toggleBtn.textContent = 'Szüneteltetés';
indicator.innerHTML = '<span class="logs-live-dot"></span> {{T "logs.elo"}}';
toggleBtn.textContent = '{{T "logs.szuneteltetes"}}';
} else {
stopPolling();
indicator.className = 'logs-live-indicator logs-live-paused';
indicator.innerHTML = 'Szünetelve';
toggleBtn.textContent = 'Folytatás';
indicator.innerHTML = '{{T "logs.szunetelve"}}';
toggleBtn.textContent = '{{T "logs.folytatas"}}';
}
};
@@ -2,57 +2,57 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Rendszermonitor</h2>
<h2>{{T "monitoring.rendszermonitor"}}</h2>
</div>
<!-- Section 0: Host (Proxmox box) health — slice 9, served by the host agent over the local API.
The de-privileged controller can't read the host itself; this card shows the real box. -->
<div class="monitor-card" id="host-health-card">
<div class="monitor-card-header">
<h3>Szerver állapota (gazdagép)</h3>
<h3>{{T "monitoring.szerver_allapota_gazdagep"}}</h3>
<span class="sysinfo-value" id="host-health-updated" style="font-size:.75rem;color:var(--text-3)"></span>
</div>
<div id="host-health-unavailable" class="monitoring-banner monitoring-banner-yellow" style="display:none">
A gazdagép metrikái jelenleg nem elérhetők.
{{T "monitoring.a_gazdagep_metrikai_jelenleg_nem"}}
</div>
<div id="host-health-body" style="display:none">
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">CPU használat</span>
<span class="sysinfo-label">{{T "monitoring.cpu_hasznalat"}}</span>
<span class="sysinfo-value" id="host-cpu">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Terhelés (load)</span>
<span class="sysinfo-label">{{T "monitoring.terheles_load"}}</span>
<span class="sysinfo-value" id="host-load">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Memória</span>
<span class="sysinfo-label">{{T "monitoring.memoria"}}</span>
<span class="sysinfo-value" id="host-mem">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">CPU hőmérséklet</span>
<span class="sysinfo-label">{{T "monitoring.cpu_homerseklet"}}</span>
<span class="sysinfo-value" id="host-temp">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Üzemidő</span>
<span class="sysinfo-label">{{T "monitoring.uzemido"}}</span>
<span class="sysinfo-value" id="host-uptime">–</span>
</div>
</div>
<h4 style="margin:1rem 0 .5rem">Tárolók kapacitása</h4>
<h4 style="margin:1rem 0 .5rem">{{T "monitoring.tarolok_kapacitasa"}}</h4>
<div class="storage-bars" id="host-storage-bars"></div>
</div>
</div>
<!-- Section 1: System Overview -->
<div class="monitor-card">
<h3>Rendszer áttekintés</h3>
<h3>{{T "monitoring.rendszer_attekintes"}}</h3>
<div class="sysinfo-grid">
<div class="sysinfo-row">
<span class="sysinfo-label">Gépnév</span>
<span class="sysinfo-label">{{T "monitoring.gepnev"}}</span>
<span class="sysinfo-value" id="sysinfo-hostname">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Operációs rendszer</span>
<span class="sysinfo-label">{{T "monitoring.operacios_rendszer"}}</span>
<span class="sysinfo-value" id="sysinfo-os">–</span>
</div>
<div class="sysinfo-row">
@@ -60,15 +60,15 @@
<span class="sysinfo-value" id="sysinfo-kernel">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Processzor</span>
<span class="sysinfo-label">{{T "monitoring.processzor"}}</span>
<span class="sysinfo-value" id="sysinfo-cpu">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Üzemidő</span>
<span class="sysinfo-label">{{T "monitoring.uzemido"}}</span>
<span class="sysinfo-value" id="sysinfo-uptime">–</span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Indítás</span>
<span class="sysinfo-label">{{T "monitoring.inditas"}}</span>
<span class="sysinfo-value" id="sysinfo-boot">–</span>
</div>
</div>
@@ -76,12 +76,12 @@
<!-- Section 1.5: Storage (moved here for visibility) -->
<div class="monitor-card">
<h3>Tárhely</h3>
<h3>{{T "monitoring.tarhely"}}</h3>
<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 "common.rendszer_gyoker"}}</span>
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
</div>
<div class="meter-track">
@@ -93,7 +93,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 "common.levalasztva"}}</span></span>
</div>
<div class="meter-track"></div>
</div>
@@ -127,15 +127,15 @@
<!-- Section 2: Hub Connection Status -->
<div class="monitor-card">
<h3>Hub kapcsolat</h3>
<h3>{{T "monitoring.hub_kapcsolat"}}</h3>
{{if .HubEnabled}}
{{if .HubConnected}}
<div class="monitoring-banner monitoring-banner-green">
Kapcsolódva — a központi rendszer aktívan figyeli a szervert.
{{T "monitoring.kapcsolodva_a_kozponti_rendszer_aktivan"}}
</div>
{{else}}
<div class="monitoring-banner monitoring-banner-red">
Nem elérhető — a központi rendszer nem kapott friss jelentést.
{{T "monitoring.nem_elerheto_a_kozponti_rendszer"}}
</div>
{{end}}
<div class="sysinfo-grid" style="margin-top: 0.75rem">
@@ -144,25 +144,25 @@
<span class="sysinfo-value"><code>{{.HubURL}}</code></span>
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Ügyfél azonosító</span>
<span class="sysinfo-label">{{T "common.ugyfel_azonosito"}}</span>
<span class="sysinfo-value"><code>{{.CustomerID}}</code></span>
</div>
{{if not .HubLastSuccess.IsZero}}
<div class="sysinfo-row">
<span class="sysinfo-label">Utolsó sikeres jelentés</span>
<span class="sysinfo-label">{{T "monitoring.utolso_sikeres_jelentes"}}</span>
<span class="sysinfo-value">{{.HubLastSuccess | timeAgo}}</span>
</div>
{{end}}
{{if .HubLastError}}
<div class="sysinfo-row">
<span class="sysinfo-label">Utolsó hiba</span>
<span class="sysinfo-label">{{T "monitoring.utolso_hiba"}}</span>
<span class="sysinfo-value"><span class="text-error">{{.HubLastError}}</span></span>
</div>
{{end}}
</div>
{{else}}
<div class="monitoring-banner monitoring-banner-yellow">
A Hub kapcsolat nincs bekapcsolva — a központi monitoring nem aktív.
{{T "monitoring.a_hub_kapcsolat_nincs_bekapcsolva"}}
</div>
{{end}}
</div>
@@ -170,41 +170,41 @@
<!-- Section 3: System Metrics Charts -->
<div class="monitor-card">
<div class="monitor-card-header">
<h3>Rendszer metrikák</h3>
<h3>{{T "monitoring.rendszer_metrikak"}}</h3>
<div class="time-range-bar" id="system-range-bar">
<button class="filter-btn active" data-range="1h">1 óra</button>
<button class="filter-btn" data-range="6h">6 óra</button>
<button class="filter-btn" data-range="24h">24 óra</button>
<button class="filter-btn" data-range="7d">7 nap</button>
<button class="filter-btn" data-range="30d">30 nap</button>
<button class="filter-btn active" data-range="1h">{{T "monitoring.1_ora"}}</button>
<button class="filter-btn" data-range="6h">{{T "monitoring.6_ora"}}</button>
<button class="filter-btn" data-range="24h">{{T "monitoring.24_ora"}}</button>
<button class="filter-btn" data-range="7d">{{T "monitoring.7_nap"}}</button>
<button class="filter-btn" data-range="30d">{{T "monitoring.30_nap"}}</button>
</div>
</div>
<div class="charts-grid" id="system-charts">
<div class="chart-box">
<div class="chart-title">CPU használat (%)</div>
<div class="chart-title">{{T "monitoring.cpu_hasznalat_2"}}</div>
<div class="chart-wrap"><canvas id="chart-cpu"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Memória használat (GB)</div>
<div class="chart-title">{{T "monitoring.memoria_hasznalat_gb"}}</div>
<div class="chart-wrap"><canvas id="chart-memory"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Hőmérséklet (°C)</div>
<div class="chart-title">{{T "monitoring.homerseklet_c"}}</div>
<div class="chart-wrap"><canvas id="chart-temp"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Terhelés (Load Average)</div>
<div class="chart-title">{{T "monitoring.terheles_load_average"}}</div>
<div class="chart-wrap"><canvas id="chart-load"></canvas></div>
</div>
</div>
<div class="chart-empty" id="system-charts-empty" style="display:none">
Még nincsenek adatok. A metrikák gyűjtése elindult, az első adatok néhány perc múlva megjelennek.
{{T "monitoring.meg_nincsenek_adatok_a_metrikak"}}
</div>
</div>
<!-- Section 3.5: Memory Distribution Bar -->
<div class="monitor-card" id="memory-distribution-card" style="display:none">
<h3>Memória eloszlás</h3>
<h3>{{T "monitoring.memoria_eloszlas"}}</h3>
<div id="mem-dist-header" class="memory-dist-header"></div>
<div class="memory-dist-bar" id="mem-dist-bar"></div>
<div class="memory-bar-legend" id="mem-dist-legend"></div>
@@ -212,19 +212,19 @@
<!-- Section 4: Container Resources -->
<div class="monitor-card">
<h3>Alkalmazás erőforrások</h3>
<h3>{{T "monitoring.alkalmazas_eroforrasok"}}</h3>
<div class="container-charts-row" id="container-charts">
<div class="chart-box chart-box-half">
<div class="chart-title">CPU használat (%)</div>
<div class="chart-title">{{T "monitoring.cpu_hasznalat_2"}}</div>
<div class="chart-wrap chart-wrap-bar"><canvas id="chart-container-cpu"></canvas></div>
</div>
<div class="chart-box chart-box-half">
<div class="chart-title">Memória használat (MB)</div>
<div class="chart-title">{{T "monitoring.memoria_hasznalat_mb"}}</div>
<div class="chart-wrap chart-wrap-bar"><canvas id="chart-container-mem"></canvas></div>
</div>
</div>
<div class="chart-empty" id="container-charts-empty" style="display:none">
Még nincsenek konténer adatok.
{{T "monitoring.meg_nincsenek_kontener_adatok"}}
</div>
</div>
@@ -233,12 +233,12 @@
<div class="monitor-card-header">
<h3 id="container-detail-title">–</h3>
<div class="time-range-bar" id="container-range-bar">
<button class="filter-btn active" data-range="1h">1 óra</button>
<button class="filter-btn" data-range="6h">6 óra</button>
<button class="filter-btn" data-range="24h">24 óra</button>
<button class="filter-btn" data-range="7d">7 nap</button>
<button class="filter-btn active" data-range="1h">{{T "monitoring.1_ora"}}</button>
<button class="filter-btn" data-range="6h">{{T "monitoring.6_ora"}}</button>
<button class="filter-btn" data-range="24h">{{T "monitoring.24_ora"}}</button>
<button class="filter-btn" data-range="7d">{{T "monitoring.7_nap"}}</button>
</div>
<button class="btn btn-outline btn-sm" onclick="closeContainerDetail()">Bezárás</button>
<button class="btn btn-outline btn-sm" onclick="closeContainerDetail()">{{T "common.bezaras"}}</button>
</div>
<div class="charts-grid charts-grid-2">
<div class="chart-box">
@@ -246,7 +246,7 @@
<div class="chart-wrap"><canvas id="chart-detail-cpu"></canvas></div>
</div>
<div class="chart-box">
<div class="chart-title">Memória (MB)</div>
<div class="chart-title">{{T "monitoring.memoria_mb"}}</div>
<div class="chart-wrap"><canvas id="chart-detail-mem"></canvas></div>
</div>
</div>
@@ -588,7 +588,7 @@
function fmtMB(mb) { return mb >= 1024 ? (mb/1024).toFixed(1) + ' GB' : Math.round(mb) + ' MB'; }
header.textContent = 'Használt: ' + fmtMB(usedMB) + ' / ' + fmtMB(totalMB) + ' (' + Math.round(usedMB/totalMB*100) + '%)';
header.textContent = '{{T "monitoring.hasznalt"}} ' + fmtMB(usedMB) + ' / ' + fmtMB(totalMB) + ' (' + Math.round(usedMB/totalMB*100) + '%)';
// Sort containers by memory usage descending
containers.sort(function(a, b) { return (b.mem_usage_mb || 0) - (a.mem_usage_mb || 0); });
@@ -608,12 +608,12 @@
// OS / system overhead
if (osMB > 10) {
var osPct = (osMB / totalMB * 100).toFixed(2);
html += '<div class="memory-bar-segment" style="width:' + osPct + '%;background:var(--text-3);opacity:0.5" title="Rendszer: ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-3);opacity:0.5"></span>Rendszer (' + fmtMB(osMB) + ')</div>';
html += '<div class="memory-bar-segment" style="width:' + osPct + '%;background:var(--text-3);opacity:0.5" title="{{T "monitoring.rendszer_title"}} ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-3);opacity:0.5"></span>{{T "monitoring.rendszer_2"}}' + fmtMB(osMB) + ')</div>';
}
// Free space legend only
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--bg-2);border:1px solid var(--line)"></span>Szabad (' + fmtMB(freeMB) + ')</div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--bg-2);border:1px solid var(--line)"></span>{{T "monitoring.szabad"}}' + fmtMB(freeMB) + ')</div>';
bar.innerHTML = html;
legend.innerHTML = legendHtml;
@@ -641,7 +641,7 @@
window.showContainerDetail = async function(name) {
detailContainer = name;
document.getElementById('container-detail-title').textContent = name + ' — Erőforrás előzmények';
document.getElementById('container-detail-title').textContent = name + ' {{T "monitoring.eroforras_elozmenyek"}}';
document.getElementById('container-detail-panel').style.display = '';
document.getElementById('container-detail-panel').scrollIntoView({behavior: 'smooth'});
await loadContainerDetail();
@@ -745,7 +745,7 @@
var bars = document.getElementById('host-storage-bars');
var targets = Array.isArray(d.storage_targets) ? d.storage_targets : [];
if (!targets.length) {
bars.innerHTML = '<div class="sysinfo-value" style="color:var(--text-3)">Nincs tároló adat.</div>';
bars.innerHTML = '<div class="sysinfo-value" style="color:var(--text-3)">{{T "monitoring.nincs_tarolo_adat"}}</div>';
} else {
var html = '';
targets.forEach(function(t) {
@@ -758,7 +758,7 @@
if (t.state && t.state !== 'attached') {
html += '<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">Nem elérhető</span></span></div>' +
'<span class="storage-value">— <span class="state-text-neutral">{{T "monitoring.nem_elerheto"}}</span></span></div>' +
'<div class="meter-track"></div>' + purposeHtml + '</div>';
return;
}
@@ -770,7 +770,7 @@
}
if (t.smart) {
if (t.smart.temperature_c != null) extra.push(t.smart.temperature_c + ' °C');
if (t.smart.percentage_used != null) extra.push('kopás: ' + t.smart.percentage_used + '%');
if (t.smart.percentage_used != null) extra.push('{{T "monitoring.kopas"}} ' + t.smart.percentage_used + '%');
}
var extraHtml = extra.length ? ' <span style="color:var(--text-3);font-size:.75rem">· ' + escapeHtml(extra.join(' · ')) + '</span>' : '';
html += '<div class="storage-item meter ' + usageColorClass(pct) + '">' +
@@ -786,7 +786,7 @@
var now = new Date();
document.getElementById('host-health-updated').textContent =
'Frissítve: ' + now.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit', second: '2-digit'});
'{{T "monitoring.frissitve"}} ' + now.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit', second: '2-digit'});
}
async function loadHostMetrics() {
@@ -835,9 +835,9 @@
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) return days + ' nap, ' + hours + ' óra';
if (hours > 0) return hours + ' óra, ' + minutes + ' perc';
return minutes + ' perc';
if (days > 0) return days + ' {{T "monitoring.nap"}} ' + hours + ' {{T "monitoring.ora"}}';
if (hours > 0) return hours + ' {{T "monitoring.ora_2"}} ' + minutes + ' {{T "monitoring.perc"}}';
return minutes + ' {{T "monitoring.perc"}}';
}
// =============================================
@@ -2,110 +2,110 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Beállítások — Értesítések</h2>
<h2>{{T "settings_notifications.beallitasok_ertesitesek"}}</h2>
</div>
<!-- Section C: Notification Preferences -->
<div class="settings-card">
<h3>Értesítések</h3>
<h3>{{T "settings_notifications.ertesitesek"}}</h3>
{{if .HubEnabled}}
{{if .NotificationSuccess}}<div class="alert alert-info">{{.NotificationSuccess}}</div>{{end}}
{{if .NotificationError}}<div class="alert alert-error">{{.NotificationError}}</div>{{end}}
<form method="POST" action="/settings/notifications">
{{.CSRFField}}
<div class="form-group">
<label for="notification_email">E-mail cím</label>
<label for="notification_email">{{T "settings_notifications.e_mail_cim"}}</label>
<input type="email" id="notification_email" name="notification_email"
value="{{with .NotificationPrefs}}{{.Email}}{{end}}"
placeholder="pelda@email.hu" class="form-control">
placeholder="{{T "settings_notifications.pelda_email"}}" class="form-control">
</div>
<div class="form-group">
<label>Hibák és figyelmeztetések:</label>
<label>{{T "settings_notifications.hibak_es_figyelmeztetesek"}}</label>
<div class="checkbox-group">
<label class="toggle">
<input type="checkbox" name="event_backup_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_failed"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Biztonsági mentés sikertelen</span>
<span class="toggle-label">{{T "settings_notifications.biztonsagi_mentes_sikertelen"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_db_dump_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "db_dump_failed"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Adatbázis mentés sikertelen</span>
<span class="toggle-label">{{T "settings_notifications.adatbazis_mentes_sikertelen"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_backup_integrity_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_integrity_failed"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Mentés sérülés észlelve</span>
<span class="toggle-label">{{T "settings_notifications.mentes_serules_eszlelve"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_crossdrive_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "crossdrive_failed"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Másodlagos mentés sikertelen</span>
<span class="toggle-label">{{T "settings_notifications.masodlagos_mentes_sikertelen"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_offbox_enlarge_blocked" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "offbox_enlarge_blocked"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Távoli mentés — tárhelykeret-figyelmeztetés</span>
<span class="toggle-label">{{T "settings_notifications.tavoli_mentes_tarhelykeret_figyelmeztete"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_disk_warning" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_warning"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Lemez betelőben (90% felett)</span>
<span class="toggle-label">{{T "settings_notifications.lemez_beteloben_90_felett"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_disk_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_critical"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Lemez megtelt vagy meghibásodott</span>
<span class="toggle-label">{{T "settings_notifications.lemez_megtelt_vagy_meghibasodott"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_storage_disconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_disconnected"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Meghajtó leválasztva</span>
<span class="toggle-label">{{T "settings_notifications.meghajto_levalasztva"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_node_down" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "node_down"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Szerver nem elérhető</span>
<span class="toggle-label">{{T "settings_notifications.szerver_nem_elerheto"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_health_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_critical"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Rendszer állapot kritikus</span>
<span class="toggle-label">{{T "settings_notifications.rendszer_allapot_kritikus"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_expected_backup_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_backup_missed"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Elvárt fájlmentés elmaradt</span>
<span class="toggle-label">{{T "settings_notifications.elvart_fajlmentes_elmaradt"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_expected_dbdump_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_dbdump_missed"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Elvárt adatbázismentés elmaradt</span>
<span class="toggle-label">{{T "settings_notifications.elvart_adatbazismentes_elmaradt"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_app_start_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_start_failed"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Alkalmazás nem fut</span>
<span class="toggle-label">{{T "settings_notifications.alkalmazas_nem_fut"}}</span>
</label>
</div>
</div>
<div class="form-group">
<label>Tájékoztató:</label>
<label>{{T "settings_notifications.tajekoztato"}}</label>
<div class="checkbox-group">
<label class="toggle">
<input type="checkbox" name="event_storage_reconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_reconnected"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Meghajtó újra csatlakoztatva</span>
<span class="toggle-label">{{T "settings_notifications.meghajto_ujra_csatlakoztatva"}}</span>
</label>
<label class="toggle">
<input type="checkbox" name="event_health_recovered" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_recovered"}}checked{{end}}{{end}}{{end}}>
<span class="toggle-label">Rendszer állapot helyreállt</span>
<span class="toggle-label">{{T "settings_notifications.rendszer_allapot_helyreallt"}}</span>
</label>
</div>
</div>
<div class="form-group">
<label for="cooldown_hours">Értesítési szünet</label>
<label for="cooldown_hours">{{T "settings_notifications.ertesitesi_szunet"}}</label>
<div class="form-inline">
<input type="number" id="cooldown_hours" name="cooldown_hours" min="1" max="168"
value="{{with .NotificationPrefs}}{{.CooldownHours}}{{end}}"
class="form-control form-control-narrow">
<span class="form-hint">óra (azonos probléma esetén ennyi ideig nem küld újat)</span>
<span class="form-hint">{{T "settings_notifications.ora_azonos_problema_eseten_ennyi"}}</span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Mentés</button>
<button type="submit" formaction="/settings/notifications/test" class="btn btn-outline">Teszt email küldése</button>
<button type="submit" class="btn btn-primary">{{T "settings_notifications.mentes"}}</button>
<button type="submit" formaction="/settings/notifications/test" class="btn btn-outline">{{T "settings_notifications.teszt_email_kuldese"}}</button>
</div>
</form>
{{else}}
<div class="alert alert-info">
Az értesítések a központi rendszeren keresztül működnek, ami jelenleg nincs bekapcsolva.
{{T "settings_notifications.az_ertesitesek_a_kozponti_rendszeren"}}
</div>
{{end}}
</div>
@@ -113,11 +113,9 @@
<!-- Section: App email (SMTP relay) -->
{{if .AppEmailAvailable}}
<div class="settings-card">
<h3>Alkalmazás-email</h3>
<h3>{{T "settings_notifications.alkalmazas_email"}}</h3>
<p class="settings-card-desc">
Az alkalmazások a Felhom-on keresztül küldhetnek emailt (pl. jelszó-visszaállítás, meghívók),
külön email-szolgáltató beállítása nélkül. A feladó címe minden alkalmazásnál a saját
<em>&lt;alkalmazás&gt;@felhom.eu</em> címe lesz.
{{T "settings_notifications.az_alkalmazasok_a_felhom_on"}}
</p>
{{if .AppEmailSuccess}}<div class="alert alert-info">{{.AppEmailSuccess}}</div>{{end}}
{{if .AppEmailError}}<div class="alert alert-error">{{.AppEmailError}}</div>{{end}}
@@ -126,21 +124,21 @@
<div class="form-group">
<label style="display:flex;align-items:center;gap:.5rem">
<input type="checkbox" name="app_email_enabled" value="on" {{if .AppEmailEnabled}}checked{{end}}>
Alkalmazás-email engedélyezése
{{T "settings_notifications.alkalmazas_email_engedelyezese"}}
</label>
</div>
<div class="form-group">
<label for="app_email_from_name">Feladó neve (opcionális)</label>
<label for="app_email_from_name">{{T "settings_notifications.felado_neve_opcionalis"}}</label>
<input type="text" id="app_email_from_name" name="app_email_from_name"
value="{{.AppEmailFromName}}" placeholder="pl. a háztartás neve" class="form-control">
<span class="form-hint">Ez jelenik meg a kimenő emailek feladójaként az alkalmazás neve mellett.</span>
value="{{.AppEmailFromName}}" placeholder="{{T "settings_notifications.pl_a_haztartas_neve"}}" class="form-control">
<span class="form-hint">{{T "settings_notifications.ez_jelenik_meg_a_kimeno"}}</span>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Mentés</button>
<button type="submit" class="btn btn-primary">{{T "settings_notifications.mentes"}}</button>
</div>
</form>
<p class="settings-card-desc" style="margin-top:.75rem">
Bekapcsolás után az egyes alkalmazásoknál is engedélyezni kell az email-küldést (az alkalmazás oldalán).
{{T "settings_notifications.bekapcsolas_utan_az_egyes_alkalmazasokna"}}
</p>
</div>
{{end}}
@@ -2,52 +2,51 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Beállítások — Biztonság és hozzáférés</h2>
<h2>{{T "settings_security.beallitasok_biztonsag_es_hozzaferes"}}</h2>
</div>
<!-- Section B: Password Change -->
<div class="settings-card">
<h3>Jelszó módosítás</h3>
<h3>{{T "settings_security.jelszo_modositas"}}</h3>
{{if .AuthEnabled}}
{{if .PasswordError}}<div class="alert alert-error">{{.PasswordError}}</div>{{end}}
<form method="POST" action="/settings/password">
{{.CSRFField}}
<div class="form-group">
<label for="current_password">Jelenlegi jelszó</label>
<label for="current_password">{{T "settings_security.jelenlegi_jelszo"}}</label>
<input type="password" id="current_password" name="current_password" required
placeholder="Adja meg a jelenlegi jelszavát" class="form-control">
placeholder="{{T "settings_security.adja_meg_a_jelenlegi_jelszavat"}}" class="form-control">
</div>
<div class="form-group">
<label for="new_password">Új jelszó</label>
<label for="new_password">{{T "settings_security.uj_jelszo"}}</label>
<input type="password" id="new_password" name="new_password" required minlength="8"
placeholder="Legalább 8 karakter" class="form-control">
placeholder="{{T "settings_security.legalabb_8_karakter"}}" class="form-control">
</div>
<div class="form-group">
<label for="confirm_password">Új jelszó megerősítése</label>
<label for="confirm_password">{{T "settings_security.uj_jelszo_megerositese"}}</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="8"
placeholder="Jelszó mégegyszer" class="form-control">
placeholder="{{T "settings_security.jelszo_megegyszer"}}" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Jelszó módosítása</button>
<button type="submit" class="btn btn-primary">{{T "settings_security.jelszo_modositasa"}}</button>
</form>
{{else}}
<div class="alert alert-info">
A jelszavas védelem nincs beállítva. Kérd az üzemeltetőt a beállításhoz.
{{T "settings_security.a_jelszavas_vedelem_nincs_beallitva"}}
</div>
{{end}}
</div>
<!-- Section: Geo-Restriction -->
<div class="settings-card">
<h3>Földrajzi korlátozás</h3>
<h3>{{T "settings_security.foldrajzi_korlatozas"}}</h3>
<p class="settings-card-desc">
Ország alapján korlátozható a webes alkalmazások elérése a Cloudflare WAF segítségével.
<br><span class="form-hint">A helyi hálózati hozzáférés mindig engedélyezett (nem halad át a Cloudflare-en).</span>
{{T "settings_security.orszag_alapjan_korlatozhato_a_webes"}}
<br><span class="form-hint">{{T "settings_security.a_helyi_halozati_hozzaferes_mindig"}}</span>
</p>
{{if not .CFConfigured}}
<div class="alert alert-info">
A Cloudflare API token nincs konfigurálva. Kérd az üzemeltetőt a beállításhoz.<br>
<small>A tokennek <strong>Zone WAF:Edit</strong> jogosultsággal kell rendelkeznie.</small>
{{T "settings_security.a_cloudflare_api_token_nincs"}}
</div>
{{else}}
<div id="geo-status-msg"></div>
@@ -55,16 +54,16 @@
<label class="toggle" style="margin-bottom:1rem">
<input type="checkbox" id="geo-enabled" {{if .GeoEnabled}}checked{{end}}
onchange="toggleGeo(this.checked)">
<span class="toggle-label">Geo-korlátozás aktív</span>
<span class="toggle-label">{{T "settings_security.geo_korlatozas_aktiv"}}</span>
</label>
<div id="geo-details" {{if not .GeoEnabled}}style="display:none"{{end}}>
<!-- Global allowed countries -->
<div class="form-group">
<label>Engedélyezett országok (globális)</label>
<label>{{T "settings_security.engedelyezett_orszagok_globalis"}}</label>
<div class="geo-country-selector" id="geo-countries">
<input type="text" id="geo-search" class="form-control"
placeholder="Ország keresése..."
placeholder="{{T "settings_security.orszag_keresese"}}"
autocomplete="off"
oninput="filterCountries(this.value)"
onfocus="showCountryList()"
@@ -72,22 +71,22 @@
<div class="geo-country-list" id="geo-country-list"></div>
</div>
<div class="geo-selected-tags" id="geo-selected-tags"></div>
<span class="form-hint">Csak a kiválasztott országokból érhető el a rendszer.</span>
<span class="form-hint">{{T "settings_security.csak_a_kivalasztott_orszagokbol_erheto"}}</span>
</div>
<!-- Per-app overrides -->
<div class="form-group" style="margin-top:1.5rem">
<label>Alkalmazás-specifikus felülírások</label>
<label>{{T "settings_security.alkalmazas_specifikus_felulirasok"}}</label>
<div id="geo-app-overrides"></div>
{{if .DeployedApps}}
<div style="margin-top:.5rem;display:flex;align-items:center;gap:.5rem">
<select id="geo-add-app-select" class="form-control" style="max-width:250px">
<option value="">— Alkalmazás kiválasztása —</option>
<option value="">{{T "settings_security.alkalmazas_kivalasztasa"}}</option>
{{range .DeployedApps}}
<option value="{{.Name}}">{{.Display}}</option>
{{end}}
</select>
<button class="btn btn-sm btn-outline" onclick="addAppOverride()">+ Hozzáadás</button>
<button class="btn btn-sm btn-outline" onclick="addAppOverride()">{{T "settings_security.hozzaadas"}}</button>
</div>
{{end}}
</div>
@@ -96,11 +95,11 @@
<div class="form-group" style="margin-top:1.5rem">
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<button class="btn btn-primary" id="btn-geo-save" onclick="saveGeoSettings()">
Mentés és szinkronizálás
{{T "settings_security.mentes_es_szinkronizalas"}}
</button>
<button class="btn btn-sm btn-outline" onclick="triggerGeoSync()">Kézi szinkronizálás</button>
<button class="btn btn-sm btn-outline" onclick="triggerGeoSync()">{{T "settings_security.kezi_szinkronizalas"}}</button>
<span id="geo-sync-status" class="form-hint">
{{if .GeoLastSync}}Utolsó szinkronizálás: {{.GeoLastSync}}{{end}}
{{if .GeoLastSync}}{{T "settings_security.utolso_szinkronizalas"}}{{end}}
{{if .GeoLastError}} <span class="state-text-crit">{{.GeoLastError}}</span>{{end}}
</span>
</div>
@@ -118,10 +117,10 @@ function openDialog(opts){
var root=document.getElementById('dialog-root');
if(!root) return;
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
+'<h3>'+escText(opts.title||'{{T "settings_security.megerosites"}}')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'{{T "settings_security.megerosites"}}')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">{{T "common.megsem"}}</button></div>'
+'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
@@ -171,7 +170,7 @@ function openDialog(opts){
+ escHtml(c.name) + ' <small>(' + c.code + ')</small></div>';
count++;
}
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">{{T "settings_security.nincs_talalat"}}</div>';
// Reveal with 'block', NOT '' — the .geo-country-list CSS default is display:none,
// and clearing the inline style ('') would fall back to that and keep the (populated)
// list hidden. This was the country-autocomplete "no list" bug.
@@ -188,8 +187,8 @@ function openDialog(opts){
window.removeCountry = function(code) {
if (code === 'HU') {
openDialog({title:'Figyelem', confirmLabel:'Eltávolítás',
message:'Figyelem: Magyarország eltávolítása azt jelenti, hogy magyar IP-ről sem lesz elérhető a rendszer távolról. Biztosan folytatja?',
openDialog({title:'{{T "settings_security.figyelem"}}', confirmLabel:'{{T "common.eltavolitas"}}',
message:'{{T "settings_security.figyelem_magyarorszag_eltavolitasa_azt_j"}}',
onConfirm:function(){ doRemoveCountry(code); }});
return;
}
@@ -247,8 +246,8 @@ function openDialog(opts){
var idx = ov.allowed_countries.indexOf(code);
if (idx >= 0) {
if (code === 'HU') {
openDialog({title:'Figyelem', confirmLabel:'Eltávolítás',
message:'Magyarország eltávolítása nem ajánlott. Folytatja?',
openDialog({title:'{{T "settings_security.figyelem"}}', confirmLabel:'{{T "common.eltavolitas"}}',
message:'{{T "settings_security.magyarorszag_eltavolitasa_nem_ajanlott_f"}}',
onConfirm:function(){ ov.allowed_countries.splice(ov.allowed_countries.indexOf(code), 1); },
onCancel:function(){ el.checked = true; }});
return;
@@ -262,7 +261,7 @@ function openDialog(opts){
function renderAppOverrides() {
var el = document.getElementById('geo-app-overrides');
if (!appOverrides || Object.keys(appOverrides).length === 0) {
el.innerHTML = '<p class="form-hint">Nincs alkalmazás-specifikus beállítás. Minden alkalmazás a globális beállítást követi.</p>';
el.innerHTML = '<p class="form-hint">{{T "settings_security.nincs_alkalmazas_specifikus_beallitas_mi"}}</p>';
return;
}
var html = '';
@@ -284,8 +283,8 @@ function openDialog(opts){
html += '<span class="geo-tag geo-tag-sm">' + code + '</span>';
}
html += '</div>';
html += '<button class="btn btn-sm btn-outline" onclick="editAppOverride(\'' + appName + '\')">Szerkesztés</button>';
html += '<button class="btn btn-sm btn-danger-outline" onclick="removeAppOverride(\'' + appName + '\')">Törlés</button>';
html += '<button class="btn btn-sm btn-outline" onclick="editAppOverride(\'' + appName + '\')">{{T "settings_security.szerkesztes"}}</button>';
html += '<button class="btn btn-sm btn-danger-outline" onclick="removeAppOverride(\'' + appName + '\')">{{T "common.torles"}}</button>';
html += '</div>';
}
el.innerHTML = html;
@@ -298,7 +297,7 @@ function openDialog(opts){
var checked = {};
for (var i = 0; i < ov.allowed_countries.length; i++) checked[ov.allowed_countries[i]] = true;
var html = '<div class="geo-edit-overlay" id="geo-edit-' + appName + '">';
html += '<h4>Engedélyezett országok: ' + escHtml(appName) + '</h4>';
html += '<h4>{{T "settings_security.engedelyezett_orszagok"}} ' + escHtml(appName) + '</h4>';
html += '<div class="geo-edit-grid">';
for (var i = 0; i < allCountries.length; i++) {
var c = allCountries[i];
@@ -307,7 +306,7 @@ function openDialog(opts){
+ ' ' + escHtml(c.name) + ' (' + c.code + ')</label>';
}
html += '</div>';
html += '<button class="btn btn-sm btn-primary" style="margin-top:.5rem" onclick="closeAppEdit(\'' + appName + '\')">Kész</button>';
html += '<button class="btn btn-sm btn-primary" style="margin-top:.5rem" onclick="closeAppEdit(\'' + appName + '\')">{{T "settings_security.kesz"}}</button>';
html += '</div>';
document.getElementById('geo-app-overrides').innerHTML += html;
});
@@ -324,7 +323,7 @@ function openDialog(opts){
var btn = document.getElementById('btn-geo-save');
var status = document.getElementById('geo-status-msg');
btn.disabled = true;
btn.textContent = 'Mentés...';
btn.textContent = '{{T "settings_security.mentes"}}';
var payload = {
enabled: document.getElementById('geo-enabled').checked,
@@ -339,21 +338,21 @@ function openDialog(opts){
.then(function(r){return r.json()})
.then(function(d){
if (d.ok) {
status.innerHTML = '<div class="alert alert-info">' + (d.message || 'Mentve') + '</div>';
status.innerHTML = '<div class="alert alert-info">' + (d.message || '{{T "settings_security.mentve"}}') + '</div>';
// Save per-app overrides
if (appOverrides && Object.keys(appOverrides).length > 0) {
saveAllAppOverrides();
}
} else {
status.innerHTML = '<div class="alert alert-error">' + (d.error || 'Hiba') + '</div>';
status.innerHTML = '<div class="alert alert-error">' + (d.error || '{{T "common.hiba"}}') + '</div>';
}
})
.catch(function(err){
status.innerHTML = '<div class="alert alert-error">Hálózati hiba</div>';
status.innerHTML = '<div class="alert alert-error">{{T "settings_security.halozati_hiba"}}</div>';
})
.finally(function(){
btn.disabled = false;
btn.textContent = 'Mentés és szinkronizálás';
btn.textContent = '{{T "settings_security.mentes_es_szinkronizalas"}}';
setTimeout(function(){ status.innerHTML = ''; }, 8000);
});
};
@@ -375,7 +374,7 @@ function openDialog(opts){
.then(function(r){return r.json()})
.then(function(d){
var status = document.getElementById('geo-sync-status');
status.textContent = d.ok ? 'Szinkronizálás elindítva...' : (d.error || 'Hiba');
status.textContent = d.ok ? '{{T "settings_security.szinkronizalas_elinditva"}}' : (d.error || '{{T "common.hiba"}}');
setTimeout(function(){
fetch('/api/geo/status', {headers: csrfHeaders()})
.then(function(r){return r.json()})
@@ -383,7 +382,7 @@ function openDialog(opts){
if (d.ok && d.data) {
var sync = d.data.last_sync || '';
var err = d.data.last_sync_error || '';
status.innerHTML = sync ? ('Utolsó: ' + sync.substring(0,19).replace('T',' ')) : '';
status.innerHTML = sync ? ('{{T "settings_security.utolso"}} ' + sync.substring(0,19).replace('T',' ')) : '';
if (err) status.innerHTML += ' <span class="state-text-crit">' + escHtml(err) + '</span>';
}
});
@@ -407,13 +406,13 @@ function openDialog(opts){
<!-- Section: Recovery Info -->
{{if .HasRetrievalPassword}}
<div class="settings-card">
<h3>Vészhelyzeti információk</h3>
<h3>{{T "settings_security.veszhelyzeti_informaciok"}}</h3>
<p class="settings-card-desc">
Ezeket az adatokat mentse el biztos helyre. Újratelepítés esetén szükség lesz rájuk a rendszer visszaállításához.
{{T "settings_security.ezeket_az_adatokat_mentse_el"}}
</p>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Ügyfél azonosító</span>
<span class="settings-label">{{T "common.ugyfel_azonosito"}}</span>
<span class="settings-value mono">{{.CustomerID}}</span>
</div>
<div class="settings-row">
@@ -421,22 +420,20 @@ function openDialog(opts){
<span class="settings-value mono">{{.HubURL}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Visszaállítási jelszó</span>
<span class="settings-label">{{T "settings_security.visszaallitasi_jelszo"}}</span>
<!-- R-249: the value is NOT in this page. The old markup rendered it into a
display:none span, which hid it from the eye and from nothing else — a fetch of the
page returned the plaintext. „Megjelenít" now asks the server for it. -->
<span class="settings-value">
<span id="retrieval-pw-slot" class="mono">••••••••••••••••</span>
<button type="button" id="retrieval-pw-btn" class="btn btn-xs btn-outline" onclick="revealRetrievalPw()">Megjelenít</button>
<button type="button" id="retrieval-pw-btn" class="btn btn-xs btn-outline" onclick="revealRetrievalPw()">{{T "settings_security.megjelenit"}}</button>
<span id="retrieval-pw-err" class="form-hint" style="display:none;color:var(--red)"></span>
</span>
</div>
<div class="settings-row">
<span class="settings-label">Támogatás</span>
<span class="settings-label">{{T "settings_security.tamogatas"}}</span>
<span class="settings-value">
<a href="mailto:{{.SupportEmail}}" style="color: var(--blue);">{{.SupportEmail}}</a>
&nbsp;|&nbsp;
<a href="{{.SupportURL}}" target="_blank" style="color: var(--blue);">felhom.eu/kapcsolat</a>
{{T "settings_security.felhom_eu_kapcsolat"}}
</span>
</div>
</div>
@@ -451,7 +448,7 @@ function revealRetrievalPw() {
err.style.display = 'none';
if (btn.dataset.shown === '1') { // „Elrejt" — drop the value out of the DOM again
slot.textContent = '••••••••••••••••';
btn.textContent = 'Megjelenít';
btn.textContent = '{{T "settings_security.megjelenit"}}';
btn.dataset.shown = '';
return;
}
@@ -463,16 +460,16 @@ function revealRetrievalPw() {
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) {
err.textContent = j.error || 'A visszaállítási jelszó lekérése nem sikerült.';
err.textContent = j.error || '{{T "settings_security.a_visszaallitasi_jelszo_lekerese_nem"}}';
err.style.display = 'inline';
return;
}
slot.textContent = j.data.password;
btn.textContent = 'Elrejt';
btn.textContent = '{{T "settings_security.elrejt"}}';
btn.dataset.shown = '1';
}).catch(function () {
btn.disabled = false;
err.textContent = 'A visszaállítási jelszó lekérése nem sikerült.';
err.textContent = '{{T "settings_security.a_visszaallitasi_jelszo_lekerese_nem"}}';
err.style.display = 'inline';
});
}
@@ -2,20 +2,20 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Beállítások — Rendszer</h2>
<h2>{{T "settings_system.beallitasok_rendszer"}}</h2>
</div>
<!-- Section A: System Configuration (read-only) -->
<div class="settings-card">
<h3>Rendszer konfiguráció</h3>
<p class="settings-card-desc">Az üzemeltető által beállított értékek. Módosításhoz kérd az üzemeltetőt.</p>
<h3>{{T "settings_system.rendszer_konfiguracio"}}</h3>
<p class="settings-card-desc">{{T "settings_system.az_uzemelteto_altal_beallitott_ertekek"}}</p>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Ügyfél azonosító</span>
<span class="settings-label">{{T "common.ugyfel_azonosito"}}</span>
<span class="settings-value mono">{{.CustomerID}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Ügyfél neve</span>
<span class="settings-label">{{T "settings_system.ugyfel_neve"}}</span>
<span class="settings-value">{{.CustomerName}}</span>
</div>
<div class="settings-row">
@@ -24,27 +24,27 @@
</div>
{{if .GitRepoURL}}
<div class="settings-row">
<span class="settings-label">Alkalmazás sablon forrás</span>
<span class="settings-label">{{T "settings_system.alkalmazas_sablon_forras"}}</span>
<span class="settings-value mono settings-value-truncate">{{.GitRepoURL}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Sablon szinkronizálás</span>
<span class="settings-label">{{T "settings_system.sablon_szinkronizalas"}}</span>
<span class="settings-value mono">{{.GitSyncInterval}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Biztonsági mentés</span>
<span class="settings-value">{{if .BackupEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
<span class="settings-label">{{T "settings_system.biztonsagi_mentes"}}</span>
<span class="settings-value">{{if .BackupEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span>{{else}}<span class="state-text-neutral">{{T "settings_system.inaktiv"}}</span>{{end}}</span>
</div>
{{if .BackupEnabled}}
<div class="settings-row">
<span class="settings-label">Mentés ütemezés</span>
<span class="settings-label">{{T "settings_system.mentes_utemezes"}}</span>
<span class="settings-value mono">{{.DBDumpSchedule}} / {{.ResticSchedule}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Monitoring</span>
<span class="settings-value">{{if .MonitoringEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
<span class="settings-value">{{if .MonitoringEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span>{{else}}<span class="state-text-neutral">{{T "settings_system.inaktiv"}}</span>{{end}}</span>
</div>
{{if .MonitoringEnabled}}
<div class="settings-row">
@@ -53,78 +53,78 @@
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Hub jelentés</span>
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}–{{end}}</span>
<span class="settings-label">{{T "settings_system.hub_jelentes"}}</span>
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span>{{else}}–{{end}}</span>
</div>
</div>
</div>
<!-- Section: Version & Update -->
<div class="settings-card">
<h3>Verzió és frissítés</h3>
<h3>{{T "settings_system.verzio_es_frissites"}}</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Jelenlegi verzió</span>
<span class="settings-label">{{T "settings_system.jelenlegi_verzio"}}</span>
<span class="settings-value mono">{{.Version}}</span>
</div>
{{if .SelfUpdateEnabled}}
{{if .LatestVersion}}
<div class="settings-row">
<span class="settings-label">Legújabb verzió</span>
<span class="settings-label">{{T "settings_system.legujabb_verzio"}}</span>
<span class="settings-value mono">
{{.LatestVersion}}
{{if .UpdateAvailable}}
<span class="state-text-run" style="margin-left:0.5em;">● Frissítés elérhető</span>
<span class="state-text-run" style="margin-left:0.5em;">{{T "settings_system.frissites_elerheto"}}</span>
{{else}}
<span style="margin-left:0.5em; color:var(--text-3);">— naprakész</span>
<span style="margin-left:0.5em; color:var(--text-3);">{{T "settings_system.naprakesz"}}</span>
{{end}}
</span>
</div>
{{end}}
{{if .LastCheckTime}}
<div class="settings-row">
<span class="settings-label">Utolsó ellenőrzés</span>
<span class="settings-label">{{T "settings_system.utolso_ellenorzes"}}</span>
<span class="settings-value mono">{{.LastCheckTime}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Registry</span>
<span class="settings-value">{{if .RegistryAnonymous}}nyilvános (hitelesítés nélkül){{else}}hitelesített{{end}}</span>
<span class="settings-value">{{if .RegistryAnonymous}}{{T "settings_system.nyilvanos_hitelesites_nelkul"}}{{else}}{{T "settings_system.hitelesitett"}}{{end}}</span>
</div>
{{if .LastCheckError}}
<div class="settings-row">
<span class="settings-label">Hiba</span>
<span class="settings-label">{{T "common.hiba"}}</span>
<span class="settings-value state-text-crit">{{.LastCheckError}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Automatikus frissítés</span>
<span class="settings-label">{{T "settings_system.automatikus_frissites"}}</span>
<span class="settings-value">
{{if .AutoUpdateEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}–{{end}}
{{if .AutoUpdateEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}–{{end}}
</span>
</div>
{{if .ControllerFloor}}
<div class="settings-row">
<span class="settings-label">Minimális verzió (üzemeltető)</span>
<span class="settings-label">{{T "settings_system.minimalis_verzio_uzemelteto"}}</span>
<span class="settings-value mono">
{{.ControllerFloor}}
<span style="margin-left:0.5em; color:#888;">— a rendszer automatikusan erre a verzióra frissít, ha régebbi</span>
<span style="margin-left:0.5em; color:#888;">{{T "settings_system.a_rendszer_automatikusan_erre_a"}}</span>
</span>
</div>
{{end}}
{{if .UpdateRunning}}
<div class="settings-row">
<span class="settings-label">Állapot</span>
<span class="settings-value state-text-progress" id="auto-update-status"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Frissítés folyamatban — a vezérlő hamarosan újraindul…</span>
<span class="settings-label">{{T "settings_system.allapot"}}</span>
<span class="settings-value state-text-progress" id="auto-update-status"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> {{T "settings_system.frissites_folyamatban_a_vezerlo_hamarosa"}}</span>
</div>
{{end}}
{{with .LastUpdateState}}
<div class="settings-row">
<span class="settings-label">Utolsó frissítés</span>
<span class="settings-label">{{T "settings_system.utolso_frissites"}}</span>
<span class="settings-value">
{{if eq .Status "success"}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Sikeres</span> ({{.PreviousVersion}} → {{.TargetVersion}})
{{else if eq .Status "failed"}}<span class="state-text-crit"><svg class="ico ico-sm"><use href="#i-x"/></svg> Sikertelen</span> — {{.Error}}
{{else if eq .Status "pending"}}<span class="state-text-progress"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Folyamatban</span>
{{if eq .Status "success"}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.sikeres"}}</span> ({{.PreviousVersion}} → {{.TargetVersion}})
{{else if eq .Status "failed"}}<span class="state-text-crit"><svg class="ico ico-sm"><use href="#i-x"/></svg> {{T "settings_system.sikertelen"}}</span> — {{.Error}}
{{else if eq .Status "pending"}}<span class="state-text-progress"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> {{T "settings_system.folyamatban"}}</span>
{{end}}
</span>
</div>
@@ -132,9 +132,9 @@
<div class="settings-row" style="padding-top: 0.5em;">
<span class="settings-label"></span>
<span class="settings-value">
<button class="btn btn-secondary btn-sm" id="btn-check-update" onclick="checkUpdate()">Frissítés keresése</button>
<button class="btn btn-secondary btn-sm" id="btn-check-update" onclick="checkUpdate()">{{T "settings_system.frissites_keresese"}}</button>
{{if .UpdateAvailable}}
<button class="btn btn-primary btn-sm" id="btn-trigger-update" onclick="triggerUpdate()" style="margin-left:0.5em;">Frissítés telepítése</button>
<button class="btn btn-primary btn-sm" id="btn-trigger-update" onclick="triggerUpdate()" style="margin-left:0.5em;">{{T "settings_system.frissites_telepitese"}}</button>
{{end}}
<span id="update-status-msg" style="margin-left:0.5em; display:none;"></span>
</span>
@@ -147,50 +147,50 @@
stored NOWHERE (S-5: a DHCP lease persisted anywhere eventually misdirects people); a missing
value renders „—" because an address-less row beats a wrong address. -->
<div class="settings-card">
<h3>Hálózat</h3>
<h3>{{T "settings_system.halozat"}}</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Helyi cím (LAN)</span>
<span class="settings-value mono">{{if .NetLANAddress}}{{.NetLANAddress}}{{else}}— <span style="color:var(--text-3);">nem állapítható meg</span>{{end}}</span>
<span class="settings-label">{{T "settings_system.helyi_cim_lan"}}</span>
<span class="settings-value mono">{{if .NetLANAddress}}{{.NetLANAddress}}{{else}}— <span style="color:var(--text-3);">{{T "settings_system.nem_allapithato_meg"}}</span>{{end}}</span>
</div>
{{if .NetSMBName}}
<div class="settings-row">
<span class="settings-label">Hálózati név</span>
<span class="settings-label">{{T "settings_system.halozati_nev"}}</span>
<span class="settings-value mono">\\{{.NetSMBName}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Átjáró</span>
<span class="settings-value mono">{{if .NetGateway}}{{.NetGateway}}{{else}}— <span style="color:var(--text-3);">nem állapítható meg</span>{{end}}</span>
<span class="settings-label">{{T "settings_system.atjaro"}}</span>
<span class="settings-value mono">{{if .NetGateway}}{{.NetGateway}}{{else}}— <span style="color:var(--text-3);">{{T "settings_system.nem_allapithato_meg"}}</span>{{end}}</span>
</div>
</div>
<p class="settings-card-desc" style="margin-top:0.5em;">Ezek az értékek a helyi hálózattól függenek és változhatnak. Távoli hibaelhárításnál ezt az oldalt kérjük felolvasni.</p>
<p class="settings-card-desc" style="margin-top:0.5em;">{{T "settings_system.ezek_az_ertekek_a_helyi"}}</p>
</div>
<div class="settings-card">
<h3>Szerver memória (RAM)</h3>
<h3>{{T "settings_system.szerver_memoria_ram"}}</h3>
{{if .MemorySupported}}
{{if .MemoryReachable}}
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Jelenlegi memória</span>
<span class="settings-label">{{T "settings_system.jelenlegi_memoria"}}</span>
<span class="settings-value mono">{{.MemoryAllocatedMB}} MB</span>
</div>
<div class="settings-row">
<span class="settings-label">Felhasznált</span>
<span class="settings-label">{{T "settings_system.felhasznalt"}}</span>
<span class="settings-value mono">{{.MemoryUsageMB}} MB</span>
</div>
<div class="settings-row">
<span class="settings-label">Megengedett tartomány</span>
<span class="settings-label">{{T "settings_system.megengedett_tartomany"}}</span>
<span class="settings-value mono">{{.MemoryMinMB}} MB – {{.MemoryMaxMB}} MB</span>
</div>
<div class="settings-row" style="padding-top: 0.5em;">
<span class="settings-label">Új méret (MB)</span>
<span class="settings-label">{{T "settings_system.uj_meret_mb"}}</span>
<span class="settings-value">
<input type="number" id="mem-input" class="input mono" style="width:8em;"
value="{{.MemoryAllocatedMB}}" min="{{.MemoryMinMB}}" max="{{.MemoryMaxMB}}" step="256"
data-current="{{.MemoryAllocatedMB}}">
<button class="btn btn-primary btn-sm" id="btn-mem-resize" onclick="resizeMemory()" style="margin-left:0.5em;">Átméretezés</button>
<button class="btn btn-primary btn-sm" id="btn-mem-resize" onclick="resizeMemory()" style="margin-left:0.5em;">{{T "settings_system.atmeretezes"}}</button>
<span id="mem-status-msg" style="margin-left:0.5em; display:none;"></span>
</span>
</div>
@@ -198,14 +198,14 @@
{{else}}
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Jelenlegi memória</span>
<span class="settings-label">{{T "settings_system.jelenlegi_memoria"}}</span>
<span class="settings-value mono">{{if .MemoryProcMB}}{{.MemoryProcMB}} MB{{else}}n/a{{end}}</span>
</div>
</div>
<p class="settings-card-desc">A kiszolgáló ügynök jelenleg nem elérhető — próbáld meg később.</p>
<p class="settings-card-desc">{{T "settings_system.a_kiszolgalo_ugynok_jelenleg_nem"}}</p>
{{end}}
{{else}}
<p class="settings-card-desc">A memória átméretezéséhez a kiszolgáló rendszerfrissítése szükséges.</p>
<p class="settings-card-desc">{{T "settings_system.a_memoria_atmeretezesehez_a_kiszolgalo"}}</p>
{{end}}
</div>
@@ -218,10 +218,10 @@ function openDialog(opts){
var root=document.getElementById('dialog-root');
if(!root) return;
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
+'<h3>'+escText(opts.title||'{{T "settings_system.megerosites"}}')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'{{T "settings_system.megerosites"}}')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">{{T "common.megsem"}}</button></div>'
+'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
@@ -231,7 +231,7 @@ function checkUpdate() {
var btn = document.getElementById('btn-check-update');
var msg = document.getElementById('update-status-msg');
btn.disabled = true;
btn.textContent = 'Ellenőrzés...';
btn.textContent = '{{T "settings_system.ellenorzes"}}';
msg.style.display = 'none';
fetch('/api/selfupdate/check', {method:'POST', headers: csrfHeaders()})
.then(function(r) { return r.json(); })
@@ -239,23 +239,23 @@ function checkUpdate() {
if (data.ok) {
location.reload();
} else {
msg.textContent = data.error || 'Hiba történt';
msg.textContent = data.error || '{{T "common.hiba_tortent"}}';
msg.style.display = 'inline';
btn.disabled = false;
btn.textContent = 'Frissítés keresése';
btn.textContent = '{{T "settings_system.frissites_keresese"}}';
}
})
.catch(function() {
msg.textContent = 'Kapcsolódási hiba';
msg.textContent = '{{T "settings_system.kapcsolodasi_hiba"}}';
msg.style.display = 'inline';
btn.disabled = false;
btn.textContent = 'Frissítés keresése';
btn.textContent = '{{T "settings_system.frissites_keresese"}}';
});
}
function triggerUpdate() {
openDialog({title:'Vezérlő frissítése', confirmLabel:'Frissítés',
message:'Biztosan frissíti a controllert?\n\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.',
openDialog({title:'{{T "settings_system.vezerlo_frissitese"}}', confirmLabel:'{{T "settings_system.frissites"}}',
message:'{{T "settings_system.biztosan_frissiti_a_controllert_n"}}',
onConfirm:doTriggerUpdate});
}
function doTriggerUpdate() {
@@ -263,25 +263,25 @@ function doTriggerUpdate() {
var checkBtn = document.getElementById('btn-check-update');
var msg = document.getElementById('update-status-msg');
btn.disabled = true;
btn.textContent = 'Frissítés...';
btn.textContent = '{{T "settings_system.frissites_2"}}';
if (checkBtn) checkBtn.disabled = true;
msg.textContent = 'Frissítés folyamatban...';
msg.textContent = '{{T "settings_system.frissites_folyamatban"}}';
msg.style.display = 'inline';
fetch('/api/selfupdate/update', {method:'POST', headers: csrfHeaders()})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
msg.textContent = 'Újraindulás...';
msg.textContent = '{{T "settings_system.ujraindulas"}}';
pollUntilBack();
} else {
msg.textContent = data.error || 'Hiba történt';
msg.textContent = data.error || '{{T "common.hiba_tortent"}}';
btn.disabled = false;
btn.textContent = 'Frissítés telepítése';
btn.textContent = '{{T "settings_system.frissites_telepitese"}}';
if (checkBtn) checkBtn.disabled = false;
}
})
.catch(function() {
msg.textContent = 'Kapcsolódási hiba';
msg.textContent = '{{T "settings_system.kapcsolodasi_hiba"}}';
pollUntilBack();
});
}
@@ -291,12 +291,12 @@ function resizeMemory() {
var target = parseInt(input.value, 10);
var current = parseInt(input.getAttribute('data-current'), 10);
if (isNaN(target) || target <= 0) {
showMemMsg('Érvénytelen memória érték', true);
showMemMsg('{{T "settings_system.ervenytelen_memoria_ertek"}}', true);
return;
}
if (target < current) {
openDialog({title:'Memória csökkentése', confirmLabel:'Csökkentés',
message:'A memória csökkentése hatással lehet a futó alkalmazások teljesítményére. Biztosan folytatod?',
openDialog({title:'{{T "settings_system.memoria_csokkentese"}}', confirmLabel:'{{T "settings_system.csokkentes"}}',
message:'{{T "settings_system.a_memoria_csokkentese_hatassal_lehet"}}',
onConfirm:function(){ doResizeMemory(target); }});
return;
}
@@ -305,7 +305,7 @@ function resizeMemory() {
function doResizeMemory(target) {
var btn = document.getElementById('btn-mem-resize');
btn.disabled = true;
btn.textContent = 'Átméretezés...';
btn.textContent = '{{T "settings_system.atmeretezes_2"}}';
fetch('/api/system/memory/resize', {
method:'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
body: JSON.stringify({memory_mb: target})
@@ -313,18 +313,18 @@ function doResizeMemory(target) {
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
showMemMsg(data.error || data.message || 'Kész.', false);
showMemMsg(data.error || data.message || '{{T "settings_system.kesz"}}', false);
setTimeout(function(){ location.reload(); }, 1200);
} else {
showMemMsg(data.error || 'A művelet nem sikerült', true);
showMemMsg(data.error || '{{T "settings_system.a_muvelet_nem_sikerult"}}', true);
btn.disabled = false;
btn.textContent = 'Átméretezés';
btn.textContent = '{{T "settings_system.atmeretezes"}}';
}
})
.catch(function() {
showMemMsg('Kapcsolódási hiba', true);
showMemMsg('{{T "settings_system.kapcsolodasi_hiba"}}', true);
btn.disabled = false;
btn.textContent = 'Átméretezés';
btn.textContent = '{{T "settings_system.atmeretezes"}}';
});
}
function showMemMsg(text, isErr) {
@@ -357,50 +357,49 @@ pollUntilBack();
<!-- Section: Controller restart (self-serve) — always available (not gated on RetrievalPassword) -->
<div class="settings-card">
<h3>Vezérlő újraindítása</h3>
<h3>{{T "settings_system.vezerlo_ujrainditasa"}}</h3>
<p class="settings-card-desc">
Ha a vezérlő hibásan működik, itt biztonságosan újraindíthatja — nem kell az egész szervert újraindítani.
Az alkalmazásai futnak tovább; csak a vezérlő indul újra (néhány másodperc).
{{T "settings_system.ha_a_vezerlo_hibasan_mukodik"}}
</p>
<div id="restart-status"></div>
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Vezérlő újraindítása</button>
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">{{T "settings_system.vezerlo_ujrainditasa"}}</button>
</div>
<!-- Section: Full server (guest) restart — a deliberate maintenance affordance, sibling to the controller restart -->
<div class="settings-card">
<h3>Kiszolgáló újraindítása</h3>
<h3>{{T "settings_system.kiszolgalo_ujrainditasa"}}</h3>
<p class="settings-card-desc">
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor használja, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő.
{{T "settings_system.az_egesz_kiszolgalo_szerver_ujrainditasa"}}
</p>
<div id="server-restart-status"></div>
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">Kiszolgáló újraindítása</button>
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">{{T "settings_system.kiszolgalo_ujrainditasa"}}</button>
</div>
<script>
function restartController() {
openDialog({title:'Vezérlő újraindítása', confirmLabel:'Újraindítás',
message:'Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.',
openDialog({title:'{{T "settings_system.vezerlo_ujrainditasa"}}', confirmLabel:'{{T "common.ujrainditas"}}',
message:'{{T "settings_system.biztosan_ujrainditja_a_vezerlot_a"}}',
onConfirm:doRestartController});
}
function doRestartController() {
var btn = document.getElementById('btn-restart-controller');
var status = document.getElementById('restart-status');
btn.disabled = true;
status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… újracsatlakozás…</div>';
status.innerHTML = '<div class="alert alert-info">{{T "settings_system.ujrainditas_folyamatban_ujracsatlakozas"}}</div>';
fetch('/api/selfrestart', { method: 'POST', headers: csrfHeaders() })
.then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
}
function restartServer() {
openDialog({title:'Kiszolgáló újraindítása', confirmLabel:'Újraindítás',
message:'Biztosan újraindítja a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.',
openDialog({title:'{{T "settings_system.kiszolgalo_ujrainditasa"}}', confirmLabel:'{{T "common.ujrainditas"}}',
message:'{{T "settings_system.biztosan_ujrainditja_a_kiszolgalot_az"}}',
onConfirm:doRestartServer});
}
function doRestartServer() {
var btn = document.getElementById('btn-restart-server');
var status = document.getElementById('server-restart-status');
if (btn) btn.disabled = true;
if (status) status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… a vezérlőpult néhány másodperc múlva újratölt.</div>';
if (status) status.innerHTML = '<div class="alert alert-info">{{T "settings_system.ujrainditas_folyamatban_a_vezerlopult_ne"}}</div>';
fetch('/api/server/reboot', { method: 'POST', headers: csrfHeaders() })
.then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless
@@ -408,7 +407,7 @@ function doRestartServer() {
function pollRestart(attempt) {
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
document.getElementById('restart-status').innerHTML =
'<div class="alert alert-error">Az újraindítás a vártnál tovább tart. Töltse újra az oldalt kézzel.</div>';
'<div class="alert alert-error">{{T "settings_system.az_ujrainditas_a_vartnal_tovabb"}}</div>';
return;
}
setTimeout(function(){
+31 -31
View File
@@ -2,18 +2,18 @@
{{template "layout_start" .}}
<div class="page-header">
<h2>Alkalmazások</h2>
<h2>{{T "stacks.alkalmazasok"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
<a href="/import" class="btn btn-sm btn-outline" title="Alkalmazás visszaállítása exportált csomagból">Importálás</a>
<button class="btn btn-sm btn-outline" id="sync-btn" onclick="syncTemplates()" title="Sablonok frissítése a központi katalógusból">↻ Sablonok frissítése</button>
<a href="/import" class="btn btn-sm btn-outline" title="{{T "stacks.alkalmazas_visszaallitasa_exportalt_csom"}}">{{T "stacks.importalas"}}</a>
<button class="btn btn-sm btn-outline" id="sync-btn" onclick="syncTemplates()" title="{{T "stacks.sablonok_frissitese_a_kozponti_katalogus"}}">{{T "stacks.sablonok_frissitese"}}</button>
</div>
<div id="sync-toast" class="sync-toast" style="display:none"></div>
<div class="filter-bar" id="filter-bar">
<button class="filter-btn active" data-filter="all">Mind <span class="filter-count" id="count-all"></span></button>
<button class="filter-btn" data-filter="running">Futó <span class="filter-count" id="count-running"></span></button>
<button class="filter-btn" data-filter="stopped">Leállítva <span class="filter-count" id="count-stopped"></span></button>
<button class="filter-btn" data-filter="available">Telepíthető <span class="filter-count" id="count-available"></span></button>
<button class="filter-btn active" data-filter="all">{{T "stacks.mind"}} <span class="filter-count" id="count-all"></span></button>
<button class="filter-btn" data-filter="running">{{T "stacks.futo"}} <span class="filter-count" id="count-running"></span></button>
<button class="filter-btn" data-filter="stopped">{{T "stacks.leallitva"}} <span class="filter-count" id="count-stopped"></span></button>
<button class="filter-btn" data-filter="available">{{T "stacks.telepitheto"}} <span class="filter-count" id="count-available"></span></button>
</div>
<div class="stack-grid">
@@ -32,18 +32,18 @@
{{$subdomain}}.{{$.Domain}} ↗
</a>
{{if and .Deployed (routeUnpublished .State)}}
<span class="route-unpublished" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL nem érhető el (404), pedig a konténer fut."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> URL nem elérhető – útvonal nincs publikálva</span>
<span class="route-unpublished" title="{{T "stacks.a_proxy_traefik_csak_egeszseges"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "stacks.url_nem_elerheto_utvonal_nincs"}}</span>
{{end}}
{{end}}
</div>
</div>
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
{{if .Orphaned}}<span class="tag tag-warn">{{T "common.elavult"}}</span>{{end}}
{{template "meta_badge" (lifecycleBadge .Meta)}}
{{template "meta_badge" (updateBadge .)}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hiányzó tárhely: {{$ms}}</span>{{end}}
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="Az alkalmazás környezetében a hálózati tárhely helyén üres helyi könyvtár van — az adatok nem a NAS-ra kerülnek. Jelezze az üzemeltetőnek."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely hibás — az alkalmazás nem a NAS-t látja</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="{{T "stacks.az_alkalmazas_adattaroloja_nem_elerheto"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "stacks.hianyzo_tarhely"}}</span>{{end}}
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="{{T "stacks.az_alkalmazas_kornyezeteben_a_halozati"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "stacks.halozati_tarhely_hibas_az_alkalmazas"}}</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="{{T "stacks.a_halozati_tarhely_nas_jelenleg"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "stacks.halozati_tarhely_nem_elerheto"}}</span>{{end}}
</div>
{{if $im}}
@@ -54,10 +54,10 @@
<div class="metarows">
{{if .Meta.Resources.MemRequest}}<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~{{.Meta.Resources.MemRequest}}</span></span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>{{end}}
{{if and .Deployed (index $.StorageLabels .Name)}}<span class="metarow" title="Adattároló: {{index $.StorageLabels .Name}}"><svg class="ico"><use href="#i-hard-drive"/></svg>{{index $.StorageLabels .Name}}</span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>{{T "common.pi_kompatibilis"}}</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>{{T "common.hdd_szukseges"}}</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>{{T "common.magyar_felulet"}}</span>{{end}}
{{if and .Deployed (index $.StorageLabels .Name)}}<span class="metarow" title="{{T "stacks.adattarolo"}}"><svg class="ico"><use href="#i-hard-drive"/></svg>{{index $.StorageLabels .Name}}</span>{{end}}
</div>
{{if .Containers}}
@@ -73,18 +73,18 @@
<div class="stack-detail-actions">
{{if .Protected}}
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>Védett rendszerkomponens</span>
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>{{T "stacks.vedett_rendszerkomponens"}}</span>
{{if isOperational .State}}
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">Újraindítás</button>
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>
{{end}}
{{if .Meta.Slug}}
<a href="/apps/{{.Meta.Slug}}" class="btn btn-outline">Részletek</a>
<a href="/apps/{{.Meta.Slug}}" class="btn btn-outline">{{T "stacks.reszletek"}}</a>
{{end}}
{{else if not .Deployed}}
{{/* The endpoint refuses a non-installable template server-side; hiding the button
here keeps the two consistent rather than offering an action that will fail. */}}
{{if canInstall .Meta}}<a href="/stacks/{{.Name}}/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>{{end}}
<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">Részletek</a>
{{if canInstall .Meta}}<a href="/stacks/{{.Name}}/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">{{T "stacks.telepites"}}</a>{{end}}
<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">{{T "stacks.reszletek"}}</a>
{{else}}
{{/* Slice 4 (v0.238.0): the UPDATING and HELD checks come BEFORE isOperational. That
predicate counts `restarting` as operational, which is how the 2026-09-01 spike saw
@@ -93,21 +93,21 @@
{{if .Updating}}
<span class="tag tag-progress" data-update-phase="{{.UpdatePhase}}"><span class="dot"></span>{{.UpdatePhaseLabel}}</span>
{{else if .HoldReason}}
<div class="alert alert-error" data-held="true">{{.HoldReason}} <a href="/backups/apps" class="btn btn-sm btn-outline">Mentések</a></div>
{{if not .Orphaned}}<button class="btn btn-danger" onclick="removeStack('{{.Name}}')">Eltávolítás</button>{{end}}
<div class="alert alert-error" data-held="true">{{.HoldReason}} <a href="/backups/apps" class="btn btn-sm btn-outline">{{T "stacks.mentesek"}}</a></div>
{{if not .Orphaned}}<button class="btn btn-danger" onclick="removeStack('{{.Name}}')">{{T "common.eltavolitas"}}</button>{{end}}
{{else if isOperational .State}}
{{if .UpdateError}}<div class="alert alert-warning" data-update-error="true">{{.UpdateError}}</div>{{end}}
{{if not .Orphaned}}<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'update')">Frissítés</button>{{end}}
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">Újraindítás</button>
<button class="btn btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')">Leállítás</button>
{{if not .Orphaned}}<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'update')">{{T "stacks.frissites"}}</button>{{end}}
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>
<button class="btn btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')">{{T "stacks.leallitas"}}</button>
{{else}}
{{if .UpdateError}}<div class="alert alert-warning" data-update-error="true">{{.UpdateError}}</div>{{end}}
<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'start')">Indítás</button>
{{if not .Orphaned}}<button class="btn btn-danger" onclick="removeStack('{{.Name}}')">Eltávolítás</button>{{end}}
<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'start')">{{T "stacks.inditas"}}</button>
{{if not .Orphaned}}<button class="btn btn-danger" onclick="removeStack('{{.Name}}')">{{T "common.eltavolitas"}}</button>{{end}}
{{end}}
<a href="/stacks/{{.Name}}/logs" class="btn btn-outline">Naplók</a>
{{if not .Orphaned}}<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">Részletek</a>{{end}}
{{if .Orphaned}}<button class="btn btn-danger" onclick="deleteOrphanStack('{{.Name}}')">Törlés</button>{{end}}
<a href="/stacks/{{.Name}}/logs" class="btn btn-outline">{{T "stacks.naplok"}}</a>
{{if not .Orphaned}}<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">{{T "stacks.reszletek"}}</a>{{end}}
{{if .Orphaned}}<button class="btn btn-danger" onclick="deleteOrphanStack('{{.Name}}')">{{T "common.torles"}}</button>{{end}}
{{end}}
</div>
</div>