v0.252.0 — the sentences the program builds follow the language (R-557 slice 2 release A)
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still Hungarian literals in Go, so an English household clicked an English button and was answered in Hungarian. 226 of them move into the bundle here. A flash was the hard part: it travels inside the redirect URL and is rendered by a DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by an older controller carries prose and is shown verbatim — never a raw key, never dropped. Also converted: page data and view-model text, the internal/api JSON answers, the alert banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at display, and the four page titles built around an app name (R-566 closed). Hungarian is byte-identical, and that is measured rather than read: scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict. Its own first version filtered the capture through an ASCII-Hungarian word list and missed seven real literals — the R-565 class. The filter is gone. Nothing on the wire moved, and wire goldens now hold it there: the report's health warnings and every notify event message stay Hungarian, because the hub MAILS the controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
+110
-107
@@ -18,6 +18,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/infra"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
@@ -47,11 +48,11 @@ type StorageBarInfo struct {
|
||||
// monitoring "Tárolók kapacitása" list. These are all external/user-data drives (the agent's
|
||||
// system/PBS storage is not in the controller's storage-path registry), matching the user-data
|
||||
// purpose text on the storage-management page (Phase 4C).
|
||||
const storageBarPurpose = "Külső adattároló — a telepített alkalmazások nagy méretű fájljai (média, dokumentumok) ide kerülnek; az adatbázisok a belső SSD-n vannak."
|
||||
const storageBarPurposeKey = "storage.bar_purpose"
|
||||
|
||||
// buildStorageBars returns usage bars for all registered storage paths, in a stable order
|
||||
// (by path) with a purpose description.
|
||||
func (s *Server) buildStorageBars() []StorageBarInfo {
|
||||
func (s *Server) buildStorageBars(lang string) []StorageBarInfo {
|
||||
var bars []StorageBarInfo
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
// Skip decommissioned drives — they are no longer in active use
|
||||
@@ -62,7 +63,7 @@ func (s *Server) buildStorageBars() []StorageBarInfo {
|
||||
bars = append(bars, StorageBarInfo{
|
||||
Label: sp.Label,
|
||||
Path: sp.Path,
|
||||
Purpose: storageBarPurpose,
|
||||
Purpose: s.msgLang(lang, storageBarPurposeKey),
|
||||
Disconnected: true,
|
||||
})
|
||||
continue
|
||||
@@ -74,7 +75,7 @@ func (s *Server) buildStorageBars() []StorageBarInfo {
|
||||
bars = append(bars, StorageBarInfo{
|
||||
Label: sp.Label,
|
||||
Path: sp.Path,
|
||||
Purpose: storageBarPurpose,
|
||||
Purpose: s.msgLang(lang, storageBarPurposeKey),
|
||||
TotalGB: di.TotalGB,
|
||||
UsedGB: di.UsedGB,
|
||||
Percent: di.UsedPercent,
|
||||
@@ -134,7 +135,9 @@ func (s *Server) baseData(page, title string) map[string]interface{} {
|
||||
"ClaimLegacyOpen": s.claimLegacyOpen(),
|
||||
}
|
||||
if s.alertManager != nil {
|
||||
data["Alerts"] = s.alertManager.GetAlerts()
|
||||
// Hungarian here; addLanguageData re-renders the set in the request's language, because
|
||||
// baseData has no request and every page goes through executeTemplate (v0.252.0, R-557).
|
||||
data["Alerts"] = s.alertManager.GetAlerts(i18n.Default)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -189,7 +192,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["StoppedCount"] = stopped
|
||||
data["TotalCount"] = len(stackList)
|
||||
data["SystemInfo"] = sysInfo
|
||||
data["StorageBars"] = s.buildStorageBars()
|
||||
data["StorageBars"] = s.buildStorageBars(s.langFor(r))
|
||||
|
||||
// Disk-health card (v0.169.0) — physical-disk SMART verdicts via the 60s-TTL-cached /disks call.
|
||||
// Never blocks the render: an unreachable agent yields nil rows and the card shows its empty state.
|
||||
@@ -214,7 +217,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["Subdomains"] = s.subdomainMap(deployedStacks)
|
||||
|
||||
if s.alertManager != nil {
|
||||
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("dashboard")
|
||||
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("dashboard", s.langFor(r))
|
||||
}
|
||||
|
||||
s.executeTemplate(w, r, "dashboard", data)
|
||||
@@ -328,7 +331,7 @@ func (s *Server) launcherHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["ShareURL"] = "https://" + r.Host + "/s/" + token
|
||||
}
|
||||
data["SharePasswordSet"] = s.settings.GetLauncherSharePasswordHash() != ""
|
||||
if f := strings.TrimSpace(r.URL.Query().Get("flash")); f != "" {
|
||||
if f := s.flashFrom(r, "flash"); f != "" {
|
||||
data["ShareFlash"] = f
|
||||
}
|
||||
s.executeTemplate(w, r, "launcher", data)
|
||||
@@ -400,7 +403,7 @@ func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string
|
||||
|
||||
logs, err := s.stackMgr.GetLogs(name, 200)
|
||||
if err != nil {
|
||||
logs = fmt.Sprintf("Hiba a naplók lekérésekor: %v", err)
|
||||
logs = s.msg(r, "logs.fetch_failed", err)
|
||||
}
|
||||
|
||||
// Raw mode: return plain text for AJAX polling
|
||||
@@ -411,6 +414,7 @@ func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string
|
||||
}
|
||||
|
||||
data := s.baseData("logs", stack.Meta.DisplayName+" — Naplók")
|
||||
data["TitleKey"], data["TitleArgs"] = "page.title.logs", []interface{}{stack.Meta.DisplayName} // i18n: the Hungarian title above is what hu renders
|
||||
data["Stack"] = stack
|
||||
data["Logs"] = logs
|
||||
s.executeTemplate(w, r, "logs", data)
|
||||
@@ -432,11 +436,12 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
||||
stack, _ := s.stackMgr.GetStack(name)
|
||||
alreadyDeployed := appCfg != nil && appCfg.Deployed
|
||||
|
||||
pageTitle := meta.DisplayName + " — Telepítés"
|
||||
pageTitle, pageTitleKey := meta.DisplayName+" — Telepítés", "page.title.deploy"
|
||||
if alreadyDeployed {
|
||||
pageTitle = meta.DisplayName + " — Beállítások"
|
||||
pageTitle, pageTitleKey = meta.DisplayName+" — Beállítások", "page.title.app_settings"
|
||||
}
|
||||
data := s.baseData("deploy", pageTitle)
|
||||
data["TitleKey"], data["TitleArgs"] = pageTitleKey, []interface{}{meta.DisplayName} // i18n: the Hungarian title above is what hu renders
|
||||
data["Stack"] = stack
|
||||
data["Meta"] = meta
|
||||
data["AppConfig"] = appCfg
|
||||
@@ -522,7 +527,7 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
||||
// the honest UI over it, and it must not be mistaken for the boundary itself.
|
||||
if refuse, _ := s.settings.RefuseAsAppNamespace(sp.Path); refuse {
|
||||
dp.NotAllowed = true
|
||||
dp.NotAllowedNote = "hálózati tárhely — alkalmazáshoz nem választható"
|
||||
dp.NotAllowedNote = s.msg(r, "deploy.path_not_allowed")
|
||||
}
|
||||
if di := system.GetDiskUsage(sp.Path); di != nil {
|
||||
dp.FreeHuman = formatFreeSpace(di.AvailGB)
|
||||
@@ -671,10 +676,10 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
||||
}
|
||||
|
||||
// Flash messages from cross-drive backup save redirect
|
||||
if flash := r.URL.Query().Get("flash"); flash != "" {
|
||||
if flash := s.flashFrom(r, "flash"); flash != "" {
|
||||
data["FlashSuccess"] = flash
|
||||
}
|
||||
if flashErr := r.URL.Query().Get("flash_error"); flashErr != "" {
|
||||
if flashErr := s.flashFrom(r, "flash_error"); flashErr != "" {
|
||||
data["FlashError"] = flashErr
|
||||
}
|
||||
|
||||
@@ -754,7 +759,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
case settings.FileBrowserAdminGenerated:
|
||||
data["HasAppInfo"] = true
|
||||
data["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin",
|
||||
Note: "A Fájlkezelő belépése. A jelszót a Felhom állította be ezen a gépen."}
|
||||
Note: s.msg(r, "creds.filebrowser_note")}
|
||||
data["InitialCredsHasPassword"] = enc != ""
|
||||
case settings.FileBrowserAdminOperator:
|
||||
data["HasAppInfo"] = true
|
||||
@@ -797,11 +802,11 @@ 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()
|
||||
data["StorageBars"] = s.buildStorageBars(s.langFor(r))
|
||||
|
||||
if s.alertManager != nil {
|
||||
data["Alerts"] = s.alertManager.GetAlerts()
|
||||
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("monitoring")
|
||||
data["Alerts"] = s.alertManager.GetAlerts(s.langFor(r))
|
||||
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("monitoring", s.langFor(r))
|
||||
}
|
||||
|
||||
// Hub connection status section
|
||||
@@ -824,11 +829,11 @@ func (s *Server) monitoringHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
|
||||
if s.cfg.Monitoring.Enabled {
|
||||
pings := []map[string]interface{}{
|
||||
{"Label": "Eletjel (Heartbeat)", "Icon": "heartbeat", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Heartbeat), "Schedule": "5 percenkent"},
|
||||
{"Label": "Rendszer allapot", "Icon": "system", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.SystemHealth), "Schedule": "5 percenkent"},
|
||||
{"Label": "Adatbazis mentes", "Icon": "db", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.DBDump), "Schedule": "Naponta " + s.cfg.Backup.DBDumpSchedule},
|
||||
{"Label": "Biztonsagi mentes", "Icon": "backup", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Backup), "Schedule": "Naponta " + s.cfg.Backup.ResticSchedule},
|
||||
{"Label": "Mentes integritas", "Icon": "integrity", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.BackupIntegrity), "Schedule": monitoringIntegritySchedule},
|
||||
{"Label": s.msg(r, "ping.label.heartbeat"), "Icon": "heartbeat", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Heartbeat), "Schedule": s.msg(r, "ping.schedule.5min")},
|
||||
{"Label": s.msg(r, "ping.label.system_health"), "Icon": "system", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.SystemHealth), "Schedule": s.msg(r, "ping.schedule.5min")},
|
||||
{"Label": s.msg(r, "ping.label.db_dump"), "Icon": "db", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.DBDump), "Schedule": s.msg(r, "ping.schedule.daily_at", s.cfg.Backup.DBDumpSchedule)},
|
||||
{"Label": s.msg(r, "ping.label.backup"), "Icon": "backup", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Backup), "Schedule": s.msg(r, "ping.schedule.daily_at", s.cfg.Backup.ResticSchedule)},
|
||||
{"Label": s.msg(r, "ping.label.integrity"), "Icon": "integrity", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.BackupIntegrity), "Schedule": monitoringIntegritySchedule},
|
||||
}
|
||||
allConfigured := true
|
||||
for _, p := range pings {
|
||||
@@ -863,10 +868,10 @@ func (s *Server) backupsCommonData(page, title string, r *http.Request) map[stri
|
||||
fullStatus := s.backupMgr.GetFullStatus(nextDBDump)
|
||||
|
||||
// Pass flash messages from query params (set by redirect handlers)
|
||||
if flash := r.URL.Query().Get("flash"); flash != "" {
|
||||
if flash := s.flashFrom(r, "flash"); flash != "" {
|
||||
fullStatus.FlashSuccess = flash
|
||||
}
|
||||
if flashErr := r.URL.Query().Get("flash_error"); flashErr != "" {
|
||||
if flashErr := s.flashFrom(r, "flash_error"); flashErr != "" {
|
||||
fullStatus.FlashError = flashErr
|
||||
}
|
||||
data["Backup"] = fullStatus
|
||||
@@ -875,7 +880,7 @@ func (s *Server) backupsCommonData(page, title string, r *http.Request) map[stri
|
||||
|
||||
// backupsOffboxData adds the offbox target + per-app toggle state (the remote, apps and restore
|
||||
// pages all render some of it: status card / toggle list / tier-3 rows / restore-to-verify).
|
||||
func (s *Server) backupsOffboxData(data map[string]interface{}) {
|
||||
func (s *Server) backupsOffboxData(data map[string]interface{}, lang string) {
|
||||
offboxTgt := s.settings.GetOffboxTarget()
|
||||
data["Offbox"] = offboxTgt
|
||||
data["OffboxConfigured"] = s.backupMgr != nil && s.backupMgr.OffboxConfigured()
|
||||
@@ -897,7 +902,7 @@ func (s *Server) backupsOffboxData(data map[string]interface{}) {
|
||||
data["OffboxToggledCount"] = offboxToggled
|
||||
// Part E (v0.126.0): the LastWarning DISPLAY pick — never a state mutation.
|
||||
if offboxTgt != nil {
|
||||
data["OffboxWarningDisplay"] = offboxWarningDisplay(offboxTgt.LastWarning, offboxTgt.LastWarningKind, offboxToggled)
|
||||
data["OffboxWarningDisplay"] = s.offboxWarningDisplay(offboxTgt.LastWarning, offboxTgt.LastWarningKind, offboxToggled, lang)
|
||||
} else {
|
||||
data["OffboxWarningDisplay"] = ""
|
||||
}
|
||||
@@ -917,8 +922,8 @@ func (s *Server) backupsOffboxData(data map[string]interface{}) {
|
||||
// v0.251.0, when the run started recording a KIND beside it (R-553). It is kept ONLY to read boxes
|
||||
// upgraded with that older text already persisted — see offboxWarningDisplay.
|
||||
const (
|
||||
offboxStaleWarningMarker = "nincs mentésre jelölt alkalmazás"
|
||||
offboxSelectionChangedLine = "A kijelölés módosult az utolsó futás óta — a következő távoli mentés már tartalmazza."
|
||||
offboxStaleWarningMarker = "nincs mentésre jelölt alkalmazás"
|
||||
offboxSelectionChangedKey = "offbox.selection_changed"
|
||||
)
|
||||
|
||||
// offboxWarningDisplay picks what the Távoli mentés page shows for the persisted
|
||||
@@ -934,15 +939,15 @@ const (
|
||||
// R-553 legacy: remove after every fleet box has completed one off-site run on ≥ 0.251.0 (row R-570).
|
||||
// Localisation slice 2 (R-557) must not translate the producer at backup/offbox.go until that row is
|
||||
// closed — translating it while this fallback is still needed would strand exactly those boxes.
|
||||
func offboxWarningDisplay(lastWarning, kind string, toggledCount int) string {
|
||||
func (s *Server) offboxWarningDisplay(lastWarning, kind string, toggledCount int, lang string) string {
|
||||
if toggledCount < 1 {
|
||||
return lastWarning
|
||||
}
|
||||
if kind == backup.OffboxWarnNoAppsSelected {
|
||||
return offboxSelectionChangedLine
|
||||
return s.msgLang(lang, offboxSelectionChangedKey)
|
||||
}
|
||||
if kind == "" && strings.Contains(lastWarning, offboxStaleWarningMarker) {
|
||||
return offboxSelectionChangedLine
|
||||
return s.msgLang(lang, offboxSelectionChangedKey)
|
||||
}
|
||||
return lastWarning
|
||||
}
|
||||
@@ -955,7 +960,7 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// System info for storage overview bars
|
||||
data["SystemInfo"] = system.GetInfo(s.primaryHDDPath(), s.cpuCollector)
|
||||
data["StorageBars"] = s.buildStorageBars()
|
||||
data["StorageBars"] = s.buildStorageBars(s.langFor(r))
|
||||
|
||||
// Whole-guest backup view (agent-sourced, read-only) for the "Rendszermentés" section.
|
||||
data["GuestBackup"] = s.loadGuestBackup(r.Context())
|
||||
@@ -1007,7 +1012,7 @@ func offboxCeremonyWaitState(t *settings.OffboxTarget) (awaiting, timedOut bool)
|
||||
func (s *Server) backupsRemoteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.backupsCommonData("backups-remote", "Biztonsági mentés — Távoli mentés", r)
|
||||
data["TitleKey"] = "page.title.backups_remote" // i18n: the Hungarian title above is what hu renders
|
||||
s.backupsOffboxData(data)
|
||||
s.backupsOffboxData(data, s.langFor(r))
|
||||
// Escrow ceremony card states (v0.127.0): the Scenario-F stale flag + the agent version gate.
|
||||
data["EscrowStale"] = s.escrowStale()
|
||||
agentVer := ""
|
||||
@@ -1055,7 +1060,7 @@ func (s *Server) backupsRemoteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) backupsAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.backupsCommonData("backups-apps", "Biztonsági mentés — Alkalmazások", r)
|
||||
data["TitleKey"] = "page.title.backups_apps" // i18n: the Hungarian title above is what hu renders
|
||||
s.backupsOffboxData(data) // the tier-3 rows render $.Offbox status
|
||||
s.backupsOffboxData(data, s.langFor(r)) // the tier-3 rows render $.Offbox status
|
||||
|
||||
if fullStatus, ok := data["Backup"].(*backup.FullBackupStatus); ok && fullStatus != nil {
|
||||
// Enrich AppDataInfo with storage labels
|
||||
@@ -1076,7 +1081,7 @@ func (s *Server) backupsAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Build unified per-app backup rows for the app-data backup UI.
|
||||
// Disk-tier (cross-drive / restic) backup has moved to the host agent.
|
||||
data["AppBackupRows"] = s.buildAppBackupRows(fullStatus)
|
||||
data["AppBackupRows"] = s.buildAppBackupRows(fullStatus, s.langFor(r))
|
||||
data["DBSectionState"] = dbSectionState(len(fullStatus.DiscoveredDBs), len(fullStatus.DumpFiles))
|
||||
}
|
||||
|
||||
@@ -1092,7 +1097,7 @@ func (s *Server) backupsRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr != nil {
|
||||
data["InterruptedRestores"] = s.backupMgr.InterruptedRestores()
|
||||
}
|
||||
s.backupsOffboxData(data) // restore-to-verify lists the offbox-toggled apps
|
||||
s.backupsOffboxData(data, s.langFor(r)) // restore-to-verify lists the offbox-toggled apps
|
||||
// Full-restore two-step reveal (§7.2): after the size+headroom prepare step, offboxRestoreHandler
|
||||
// redirects here with the app + human size so the confirm section can show the size BEFORE starting.
|
||||
if fp := strings.TrimSpace(r.URL.Query().Get("full_prep")); fp != "" {
|
||||
@@ -1325,7 +1330,7 @@ func appDumpVerdict(dump *backup.DBDumpStatus, stackName string) string {
|
||||
// buildAppBackupRows constructs one AppBackupRow per deployed app for the backup page.
|
||||
// Disk-tier (cross-drive / restic) backup has moved to the host agent; this now
|
||||
// reflects only the app-data backup (DB dumps + Docker-volume tars).
|
||||
func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackupRow {
|
||||
func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus, lang string) []AppBackupRow {
|
||||
// Build DB stack lookup
|
||||
dbStacks := make(map[string]bool)
|
||||
for _, db := range status.DiscoveredDBs {
|
||||
@@ -1396,11 +1401,11 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackup
|
||||
if hasDB {
|
||||
base = append(base, "DB")
|
||||
}
|
||||
base = append(base, "Konfig")
|
||||
base = append(base, s.msgLang(lang, "backup.contents.db"))
|
||||
withData := func(add bool) string {
|
||||
p := append([]string{}, base...)
|
||||
if add {
|
||||
p = append(p, "Adatok")
|
||||
p = append(p, s.msgLang(lang, "backup.contents.data"))
|
||||
}
|
||||
return strings.Join(p, " + ")
|
||||
}
|
||||
@@ -1464,10 +1469,10 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackup
|
||||
|
||||
// Status dot — app-data backup status
|
||||
row.Status = "green"
|
||||
row.StatusText = "Alkalmazás-adat mentés rendben"
|
||||
row.StatusText = s.msgLang(lang, "backup.status.ok")
|
||||
if hasDB && tier1DBStatus == "error" {
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Adatbázis mentés sikertelen"
|
||||
row.StatusText = s.msgLang(lang, "backup.status.db_failed")
|
||||
}
|
||||
// R-379/R-380: a HELD app must never read as healthy. Last, so it wins over both branches
|
||||
// above — a warning beside a green tick is read as a success, and this is the one state where
|
||||
@@ -1488,12 +1493,12 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackup
|
||||
} else if cd.LastStatus == "no_target" {
|
||||
// Auto Tier 2 found no off-drive target — surface the honest reason (no silent gap).
|
||||
row.Tier2Configured = false
|
||||
row.Tier2StatusBadge = "Nincs 2. meghajtó"
|
||||
row.Tier2StatusBadge = s.msgLang(lang, "backup.tier2.no_drive")
|
||||
row.Tier2LastError = cd.LastError
|
||||
} else if cd.Enabled {
|
||||
row.Tier2Configured = true
|
||||
row.Tier2Dest = tier2DestLabel(cd.DestinationPath, s.cfg.Paths.SystemDataPath)
|
||||
row.Tier2Schedule = "Naponta"
|
||||
row.Tier2Dest = s.tier2DestLabel(cd.DestinationPath, s.cfg.Paths.SystemDataPath, lang)
|
||||
row.Tier2Schedule = s.msgLang(lang, "backup.tier2.schedule_daily")
|
||||
row.Tier2LastRun = cd.LastRun
|
||||
row.Tier2LastStatus = cd.LastStatus
|
||||
row.Tier2LastSuccess = cd.LastSuccess
|
||||
@@ -1528,11 +1533,11 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackup
|
||||
}
|
||||
switch cd.LastStatus {
|
||||
case "ok":
|
||||
row.Tier2StatusBadge = "Sikeres"
|
||||
row.Tier2StatusBadge = s.msgLang(lang, "backup.tier2.badge_ok")
|
||||
case "error":
|
||||
row.Tier2StatusBadge = "Hiba"
|
||||
row.Tier2StatusBadge = s.msgLang(lang, "backup.tier2.badge_error")
|
||||
case "running":
|
||||
row.Tier2StatusBadge = "Fut..."
|
||||
row.Tier2StatusBadge = s.msgLang(lang, "backup.tier2.badge_running")
|
||||
default:
|
||||
row.Tier2StatusBadge = "—"
|
||||
}
|
||||
@@ -1557,7 +1562,7 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackup
|
||||
Slug: u.StackName,
|
||||
StorageLabel: u.DriveLabel,
|
||||
Status: "yellow",
|
||||
StatusText: "Eltávolított alkalmazás — a mentése megvan, visszaállítható",
|
||||
StatusText: s.msgLang(lang, "backup.removed_app"),
|
||||
Removed: true,
|
||||
RemovedUnitTime: u.Time,
|
||||
Tier1LastRun: u.Time,
|
||||
@@ -1569,9 +1574,9 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackup
|
||||
|
||||
// tier2DestLabel renders a friendly destination label for the "2. mentés" card. A destination under
|
||||
// the system-data path is the internal SSD (DB/config only); otherwise it's an external drive.
|
||||
func tier2DestLabel(destPath, systemDataPath string) string {
|
||||
func (s *Server) tier2DestLabel(destPath, systemDataPath, lang string) string {
|
||||
if systemDataPath != "" && strings.HasPrefix(destPath, systemDataPath) {
|
||||
return "belső SSD (csak DB/konfiguráció)"
|
||||
return s.msgLang(lang, "backup.tier2.dest_ssd")
|
||||
}
|
||||
return filepath.Base(strings.TrimSuffix(destPath, "/"+backup.FelhomDataDir))
|
||||
}
|
||||
@@ -1620,7 +1625,7 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if s.backupMgr == nil {
|
||||
http.Redirect(w, r, "/backups/restore?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
|
||||
http.Redirect(w, r, "/backups/restore?"+flashQuery("flash_error", "flash.backup.not_configured"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
// Part B: restore is a long SYNCHRONOUS op (F4 — through cloudflared's hard 100s cap the customer
|
||||
@@ -1669,7 +1674,7 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// of it, which is the question the sentence exists to answer.
|
||||
s.backupMgr.EndRestoreOp(true, unitRestoreOutcomeMsg(stackName, res))
|
||||
}()
|
||||
http.Redirect(w, r, "/backups/restore?flash="+url.QueryEscape("Visszaállítás elindult — az állapot itt frissül."), http.StatusFound)
|
||||
http.Redirect(w, r, "/backups/restore?"+flashQuery("flash", "flash.restore.started"), http.StatusFound)
|
||||
}
|
||||
|
||||
// unitRestoreOutcomeMsg builds the OUTCOME sentence for a completed LOCAL recovery-unit restore. Pure,
|
||||
@@ -1905,7 +1910,7 @@ func (s *Server) backupTier2RestoreHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
if s.backupMgr == nil {
|
||||
http.Redirect(w, r, "/backups/apps?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
|
||||
http.Redirect(w, r, "/backups/apps?"+flashQuery("flash_error", "flash.backup.not_configured"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
// Part B (same async shape as backupRestoreHandler): fast-path refuse, then background goroutine.
|
||||
@@ -1968,7 +1973,7 @@ func (s *Server) backupTier2RestoreHandler(w http.ResponseWriter, r *http.Reques
|
||||
s.logger.Printf("[INFO] [web] Tier-2 file restore completed (async): stack=%s (%d files, legs=%v)", stackName, n, cov.Legs)
|
||||
s.backupMgr.EndRestoreOp(true, msg)
|
||||
}()
|
||||
http.Redirect(w, r, "/backups/apps?flash="+url.QueryEscape("Fájl-visszaállítás elindult — az állapot itt frissül."), http.StatusFound)
|
||||
http.Redirect(w, r, "/backups/apps?"+flashQuery("flash", "flash.restore.file_started"), http.StatusFound)
|
||||
}
|
||||
|
||||
// backupTier2UnitRestoreHandler (R-102/R-103) restores an app IN FULL from the recovery unit mirrored
|
||||
@@ -1997,7 +2002,7 @@ func (s *Server) backupTier2UnitRestoreHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
if s.backupMgr == nil {
|
||||
http.Redirect(w, r, "/backups/apps?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
|
||||
http.Redirect(w, r, "/backups/apps?"+flashQuery("flash_error", "flash.backup.not_configured"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
// R-351b (Scenario H): a second press — by button or by a direct POST — must not start a second
|
||||
@@ -2046,7 +2051,7 @@ func (s *Server) backupTier2UnitRestoreHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
s.backupMgr.EndRestoreOp(true, msg)
|
||||
}()
|
||||
http.Redirect(w, r, "/backups/apps?flash="+url.QueryEscape("Teljes visszaállítás elindult — az állapot itt frissül."), http.StatusFound)
|
||||
http.Redirect(w, r, "/backups/apps?"+flashQuery("flash", "flash.restore.full_started"), http.StatusFound)
|
||||
}
|
||||
|
||||
// settingsBaseData is the shared identity block used by every settings-family subpage
|
||||
@@ -2268,7 +2273,7 @@ func (s *Server) settingsRetrievalPasswordRevealHandler(w http.ResponseWriter, r
|
||||
// Not an error: a box that never stored one has nothing to reveal, and saying so is not a
|
||||
// leak. The page does not offer the button in that case (HasRetrievalPassword gates it).
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
escrowJSON(w, http.StatusNotFound, nil, "Ezen a gépen nincs tárolt visszaállítási jelszó.")
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.no_retrieval_password"))
|
||||
return
|
||||
}
|
||||
// The reveal is an event, and it was not one before: the same act on the hub's break-glass
|
||||
@@ -2309,18 +2314,18 @@ func (s *Server) readInitialCreds(stackName string) (*stacks.ExtractedCreds, err
|
||||
// and it is what stops this becoming "read me any value out of any app's config".
|
||||
func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Request, stackName string) {
|
||||
if s.stackMgr == nil {
|
||||
escrowJSON(w, http.StatusServiceUnavailable, nil, "Az alkalmazáskezelő nem elérhető.")
|
||||
escrowJSON(w, http.StatusServiceUnavailable, nil, s.msg(r, "escrow.stack_mgr_unavailable"))
|
||||
return
|
||||
}
|
||||
stack, ok := s.stackMgr.GetStack(stackName)
|
||||
if !ok {
|
||||
escrowJSON(w, http.StatusNotFound, nil, "Ismeretlen alkalmazás.")
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.unknown_app"))
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
envVar := strings.TrimSpace(r.FormValue("env_var"))
|
||||
if envVar == "" {
|
||||
escrowJSON(w, http.StatusBadRequest, nil, "Hiányzó mező.")
|
||||
escrowJSON(w, http.StatusBadRequest, nil, s.msg(r, "escrow.missing_field"))
|
||||
return
|
||||
}
|
||||
// AUTHORISATION: the field must be an auto-generated SECRET of this stack's catalog metadata.
|
||||
@@ -2333,18 +2338,18 @@ func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
if !allowed {
|
||||
s.logger.Printf("[WARN] [web] auto-field reveal refused for %s/%s: not an auto-generated secret field", stackName, envVar)
|
||||
escrowJSON(w, http.StatusForbidden, nil, "Ez a mező nem kérhető le.")
|
||||
escrowJSON(w, http.StatusForbidden, nil, s.msg(r, "escrow.field_not_revealable"))
|
||||
return
|
||||
}
|
||||
appCfg := s.stackMgr.LoadAppConfigByName(stackName)
|
||||
if appCfg == nil {
|
||||
escrowJSON(w, http.StatusNotFound, nil, "Az alkalmazás beállításai nem olvashatók.")
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.app_settings_unreadable"))
|
||||
return
|
||||
}
|
||||
val := crypto.DecryptMap(s.encKey, appCfg.Env)[envVar]
|
||||
if strings.TrimSpace(val) == "" {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
escrowJSON(w, http.StatusNotFound, nil, "Ehhez a mezőhöz nincs mentett érték.")
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.no_stored_value"))
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] auto-generated secret revealed for %s/%s from %s (value never logged)", stackName, envVar, clientIP(r))
|
||||
@@ -2370,7 +2375,7 @@ func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Reques
|
||||
// "your password is empty".
|
||||
func (s *Server) appInitialCredsRevealHandler(w http.ResponseWriter, r *http.Request, slug string) {
|
||||
if s.stackMgr == nil {
|
||||
escrowJSON(w, http.StatusServiceUnavailable, nil, "Az alkalmazáskezelő nem elérhető.")
|
||||
escrowJSON(w, http.StatusServiceUnavailable, nil, s.msg(r, "escrow.stack_mgr_unavailable"))
|
||||
return
|
||||
}
|
||||
// Resolved EXACTLY as appDetailHandler resolves it — same loop, same field. A second definition
|
||||
@@ -2384,7 +2389,7 @@ func (s *Server) appInitialCredsRevealHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
escrowJSON(w, http.StatusNotFound, nil, "Ismeretlen alkalmazás.")
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.unknown_app"))
|
||||
return
|
||||
}
|
||||
// R-513: the file manager's password is the controller's own stored value, not a container file.
|
||||
@@ -2396,13 +2401,12 @@ func (s *Server) appInitialCredsRevealHandler(w http.ResponseWriter, r *http.Req
|
||||
if err != nil {
|
||||
// Never swallowed, and never surfaced raw — the error can name a container/path.
|
||||
s.logger.Printf("[WARN] [web] initial-creds reveal for %s: %v", found.Name, err)
|
||||
escrowJSON(w, http.StatusBadGateway, nil, "A kezdeti jelszó beolvasása nem sikerült.")
|
||||
escrowJSON(w, http.StatusBadGateway, nil, s.msg(r, "escrow.initial_pw_read_failed"))
|
||||
return
|
||||
}
|
||||
if creds == nil || !creds.Available || strings.TrimSpace(creds.Password) == "" {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
escrowJSON(w, http.StatusNotFound, nil,
|
||||
"A kezdeti jelszó most nem olvasható ki — az alkalmazásnak futnia kell hozzá, és lehet, hogy a fájlt az első bejelentkezés után már törölték.")
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.initial_pw_unavailable"))
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] initial-credential password revealed for %s from %s (value never logged)", found.Name, clientIP(r))
|
||||
@@ -2419,18 +2423,18 @@ const fileBrowserStack = "filebrowser"
|
||||
func (s *Server) fileBrowserPasswordReveal(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if s.settings == nil {
|
||||
escrowJSON(w, http.StatusServiceUnavailable, nil, "A beállítások nem elérhetők.")
|
||||
escrowJSON(w, http.StatusServiceUnavailable, nil, s.msg(r, "escrow.settings_unavailable"))
|
||||
return
|
||||
}
|
||||
state, enc, _ := s.settings.GetFileBrowserAdmin()
|
||||
if state != settings.FileBrowserAdminGenerated || enc == "" {
|
||||
escrowJSON(w, http.StatusNotFound, nil, "A Fájlkezelő jelszavát nem a Felhom állította be ezen a gépen, ezért nem tudjuk megmutatni.")
|
||||
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.filebrowser_not_ours"))
|
||||
return
|
||||
}
|
||||
pw, err := crypto.Decrypt(s.encKey, enc)
|
||||
if err != nil || strings.TrimSpace(pw) == "" {
|
||||
s.logger.Printf("[ERROR] [web] filebrowser password reveal: decrypt failed: %v", err)
|
||||
escrowJSON(w, http.StatusInternalServerError, nil, "A jelszó most nem olvasható ki.")
|
||||
escrowJSON(w, http.StatusInternalServerError, nil, s.msg(r, "escrow.password_unreadable"))
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] filebrowser admin password revealed from %s (value never logged)", clientIP(r))
|
||||
@@ -2484,21 +2488,21 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] settingsPasswordHandler: current password mismatch from %s", r.RemoteAddr)
|
||||
}
|
||||
data["PasswordError"] = "Hibás jelenlegi jelszó"
|
||||
data["PasswordError"] = s.msg(r, "settings.pw_wrong_current")
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate new password length
|
||||
if len(newPassword) < 8 {
|
||||
data["PasswordError"] = "A jelszónak legalább 8 karakter hosszúnak kell lennie"
|
||||
data["PasswordError"] = s.msg(r, "settings.pw_too_short")
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate passwords match
|
||||
if newPassword != confirmPassword {
|
||||
data["PasswordError"] = "A két jelszó nem egyezik"
|
||||
data["PasswordError"] = s.msg(r, "settings.pw_mismatch")
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
@@ -2507,7 +2511,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), 10)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to hash new password: %v", err)
|
||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||
data["PasswordError"] = s.msg(r, "settings.pw_save_error")
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
@@ -2515,7 +2519,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
// Save to settings.json
|
||||
if err := s.settings.SetPasswordHash(string(hash)); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to save password to settings.json: %v", err)
|
||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||
data["PasswordError"] = s.msg(r, "settings.pw_save_error")
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
@@ -2526,8 +2530,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
s.invalidateAllSessions()
|
||||
|
||||
// Redirect to login with flash message
|
||||
flash := url.QueryEscape("Jelszó sikeresen módosítva. Kérjük, jelentkezzen be az új jelszóval.")
|
||||
http.Redirect(w, r, "/login?flash="+flash, http.StatusFound)
|
||||
http.Redirect(w, r, "/login?"+flashQuery("flash", "flash.login.password_changed"), http.StatusFound)
|
||||
}
|
||||
|
||||
// sameEventSet reports whether two event lists hold the same keys, ignoring order and duplicates.
|
||||
@@ -2676,7 +2679,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
||||
EnabledEvents: enabledEvents,
|
||||
CooldownHours: cooldownHours,
|
||||
}
|
||||
data["NotificationError"] = "Adj meg egy értesítési e-mail címet – bekapcsolt értesítésekhez szükséges egy cím, ahova küldhetjük őket."
|
||||
data["NotificationError"] = s.msg(r, "settings.notify_email_required")
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
@@ -2690,7 +2693,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
||||
if err := s.settings.SetNotificationPrefs(prefs); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to save notification prefs: %v", err)
|
||||
data := s.notificationsPageData()
|
||||
data["NotificationError"] = "Hiba a beállítások mentésekor"
|
||||
data["NotificationError"] = s.msg(r, "settings.notify_save_error")
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
@@ -2703,12 +2706,12 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
||||
if s.notifier != nil && s.notifier.IsEnabled() {
|
||||
if err := s.notifier.SyncPreferences(email, enabledEvents, cooldownHours); err != nil {
|
||||
s.logger.Printf("[WARN] [web] Failed to sync preferences to hub: %v", err)
|
||||
data["NotificationSuccess"] = fmt.Sprintf("Értesítési beállítások mentve (helyi). A központi szinkronizálás sikertelen: %v", err)
|
||||
data["NotificationSuccess"] = s.msg(r, "settings.notify_saved_sync_failed", err)
|
||||
} else {
|
||||
data["NotificationSuccess"] = "Értesítési beállítások mentve."
|
||||
data["NotificationSuccess"] = s.msg(r, "settings.notify_saved")
|
||||
}
|
||||
} else {
|
||||
data["NotificationSuccess"] = "Értesítési beállítások mentve."
|
||||
data["NotificationSuccess"] = s.msg(r, "settings.notify_saved")
|
||||
}
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
}
|
||||
@@ -2723,7 +2726,7 @@ func (s *Server) settingsAppEmailHandler(w http.ResponseWriter, r *http.Request)
|
||||
data := s.notificationsPageData()
|
||||
if err := s.settings.SetAppEmail(enabled, fromName); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to save app-email toggle: %v", err)
|
||||
data["AppEmailError"] = "Hiba az alkalmazás-email beállítás mentésekor"
|
||||
data["AppEmailError"] = s.msg(r, "settings.app_email_save_error")
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
@@ -2735,7 +2738,7 @@ func (s *Server) settingsAppEmailHandler(w http.ResponseWriter, r *http.Request)
|
||||
if err := s.mailShim.Apply(enabled); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] app-email shim could not be %s: %v", map[bool]string{true: "started", false: "stopped"}[enabled], err)
|
||||
data = s.notificationsPageData()
|
||||
data["AppEmailError"] = "A beállítás elmentve, de az email-szolgáltatás indítása nem sikerült."
|
||||
data["AppEmailError"] = s.msg(r, "settings.app_email_shim_failed")
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
@@ -2743,9 +2746,9 @@ func (s *Server) settingsAppEmailHandler(w http.ResponseWriter, r *http.Request)
|
||||
s.logger.Printf("[INFO] [web] App-email globally %s (from_name=%q)", map[bool]string{true: "enabled", false: "disabled"}[enabled], fromName)
|
||||
data = s.notificationsPageData()
|
||||
if enabled {
|
||||
data["AppEmailSuccess"] = "Alkalmazás-email bekapcsolva. Kapcsold be az egyes alkalmazásoknál is, ahol email-küldést szeretnél."
|
||||
data["AppEmailSuccess"] = s.msg(r, "settings.app_email_on")
|
||||
} else {
|
||||
data["AppEmailSuccess"] = "Alkalmazás-email kikapcsolva."
|
||||
data["AppEmailSuccess"] = s.msg(r, "settings.app_email_off")
|
||||
}
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
}
|
||||
@@ -2754,7 +2757,7 @@ func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http
|
||||
data := s.notificationsPageData()
|
||||
|
||||
if s.notifier == nil {
|
||||
data["NotificationError"] = "Az értesítések nincsenek bekapcsolva"
|
||||
data["NotificationError"] = s.msg(r, "settings.notify_disabled")
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
@@ -2762,12 +2765,12 @@ func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http
|
||||
err := s.notifier.SendTest()
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Test notification failed: %v", err)
|
||||
data["NotificationError"] = fmt.Sprintf("Teszt email küldése sikertelen: %v", err)
|
||||
data["NotificationError"] = s.msg(r, "settings.test_email_failed", err)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
|
||||
data["NotificationSuccess"] = "Teszt email elküldve."
|
||||
data["NotificationSuccess"] = s.msg(r, "settings.test_email_sent")
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
}
|
||||
|
||||
@@ -3049,21 +3052,21 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
||||
// 1. Exists and is directory
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil || !fi.IsDir() {
|
||||
data["StorageError"] = "Az útvonal nem létezik vagy nem mappa."
|
||||
data["StorageError"] = s.msg(r, "storage.err_path_missing")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Is mount point
|
||||
if !system.IsMountPoint(path) {
|
||||
data["StorageError"] = "Ez az útvonal nem külön csatlakoztatott meghajtó. Adatok az SSD-re kerülnének!"
|
||||
data["StorageError"] = s.msg(r, "storage.err_not_separate")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Writable
|
||||
if !system.IsWritable(path) {
|
||||
data["StorageError"] = "Az útvonal nem írható."
|
||||
data["StorageError"] = s.msg(r, "storage.err_not_writable")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
@@ -3071,7 +3074,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
||||
// 4. No overlap with existing paths
|
||||
for _, existing := range s.settings.GetStoragePaths() {
|
||||
if system.PathsOverlap(path, existing.Path) {
|
||||
data["StorageError"] = fmt.Sprintf("Az útvonal átfedi a már regisztrált %s útvonalat.", existing.Path)
|
||||
data["StorageError"] = s.msg(r, "storage.err_overlaps", existing.Path)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
@@ -3092,14 +3095,14 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
if err := s.settings.AddStoragePath(sp); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to add storage path: %v", err)
|
||||
data["StorageError"] = "Hiba a mentés során."
|
||||
data["StorageError"] = s.msg(r, "storage.err_save")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] [web] Storage path added: %s (%s)", path, label)
|
||||
go s.SyncFileBrowserMounts()
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape("Adattároló sikeresen hozzáadva: "+path), http.StatusFound)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape(s.msg(r, "storage.msg_added", path)), http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -3115,7 +3118,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
||||
// Check: apps using this path
|
||||
apps := s.appsUsingPath(path)
|
||||
if len(apps) > 0 {
|
||||
data["StorageError"] = fmt.Sprintf("Nem törölhető: az alábbi alkalmazások használják: %s", strings.Join(apps, ", "))
|
||||
data["StorageError"] = s.msg(r, "storage.err_in_use", strings.Join(apps, ", "))
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
@@ -3123,7 +3126,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
||||
// Check: cannot remove default
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
if sp.Path == path && sp.IsDefault {
|
||||
data["StorageError"] = "Az alapértelmezett adattároló nem törölhető."
|
||||
data["StorageError"] = s.msg(r, "storage.err_default")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
@@ -3131,13 +3134,13 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
// Check: last path
|
||||
if len(s.settings.GetStoragePaths()) <= 1 {
|
||||
data["StorageError"] = "Az utolsó adattároló nem törölhető."
|
||||
data["StorageError"] = s.msg(r, "storage.err_last")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.settings.RemoveStoragePath(path); err != nil {
|
||||
data["StorageError"] = "Hiba a törlés során."
|
||||
data["StorageError"] = s.msg(r, "storage.err_delete")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
@@ -3145,7 +3148,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
||||
s.logger.Printf("[INFO] [web] Storage path removed: %s", path)
|
||||
// Sync FileBrowser mounts after storage path removal
|
||||
go s.SyncFileBrowserMounts()
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape("Adattároló eltávolítva: "+path), http.StatusFound)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape(s.msg(r, "storage.msg_removed", path)), http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) settingsStorageDefaultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -3162,7 +3165,7 @@ func (s *Server) settingsStorageDefaultHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] Default storage path set to %s", path)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape("Alapértelmezett adattároló beállítva: "+path), http.StatusFound)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape(s.msg(r, "storage.msg_default_set", path)), http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) settingsStorageSchedulableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -3180,7 +3183,7 @@ func (s *Server) settingsStorageSchedulableHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] Storage schedulable updated: %s → %v", path, schedulable)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape("Adattároló állapot módosítva: "+path), http.StatusFound)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape(s.msg(r, "storage.msg_state_changed", path)), http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -3194,7 +3197,7 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
if label == "" || len(label) > 50 {
|
||||
data := s.storagePageData()
|
||||
data["StorageError"] = "A megnevezés nem lehet üres és legfeljebb 50 karakter."
|
||||
data["StorageError"] = s.msg(r, "storage.err_label")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
@@ -3202,13 +3205,13 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
|
||||
if err := s.settings.SetStorageLabel(path, label); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to set storage label: %v", err)
|
||||
data := s.storagePageData()
|
||||
data["StorageError"] = "Hiba a megnevezés mentésekor."
|
||||
data["StorageError"] = s.msg(r, "storage.err_label_save")
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] [web] Storage label updated: %s → %q", path, label)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape("Megnevezés módosítva: "+label), http.StatusFound)
|
||||
http.Redirect(w, r, "/storage?storage_msg=success&storage_detail="+url.QueryEscape(s.msg(r, "storage.msg_label_changed", label)), http.StatusFound)
|
||||
}
|
||||
|
||||
// SyncFileBrowserMounts regenerates FileBrowser's docker-compose.yml and config.yaml
|
||||
|
||||
Reference in New Issue
Block a user