v0.310.0: „A gép kikapcsolása" — the household turns the whole machine off (R-998); honest restart text (R-999)
gates / gates (push) Successful in 48s

POST /api/server/poweroff (dashboard login, POST only, the typed word KIKAPCSOLÁS / TURN OFF)
→ agent POST /host/poweroff (MinAgent 0.155.0; hidden for an older agent). The controller refuses
during its own backup/restore/app update; the agent's reasons are shown as household sentences.
„Kiszolgáló újraindítása" becomes „A Felhom újraindítása" and says it restarts the Felhom part;
/api/server/reboot is POST only (R-983). Tests red-proofed; parity fixtures regenerated on purpose.
This commit is contained in:
2026-10-13 20:17:30 +02:00
parent 1b2f74c52f
commit 201f936e9a
20 changed files with 743 additions and 41 deletions
+23
View File
@@ -1,3 +1,26 @@
## v0.310.0 — „A gép kikapcsolása": the household can turn the whole machine off (R-998); honest restart text (R-999) (2026-10-13)
**MinAgent: 0.155.0** — the new route calls the agent's `POST /host/poweroff`. An older agent: the button is hidden and
one line says an update is needed (the `host_poweroff` feature row; a 404 gets the same answer). **Hub:** v0.149.0 holds
the silence alarms for 14 days; without it the box still powers off, and the operator gets the usual outage alarms.
Design: felhom.eu `documentation/architecture/14-host-power-off.md`.
- **R-998 — the button.** Beállítások → Rendszer, after the restart cards: „A gép kikapcsolása" / "Turn the machine
off", „A gép teljesen kikapcsol. Az alkalmazásaid és a mentések leállnak. **Újra bekapcsolni csak otthon lehet: nyomd
meg a gép bekapcsológombját.**" A dialog where the household types KIKAPCSOLÁS / TURN OFF (the button stays off until
then; the server checks the word again, unaccented spelling accepted). `POST /api/server/poweroff`, dashboard login
only, POST only. The controller refuses itself during a backup, restore or app update; the agent's refusal reasons
are shown as household sentences (`poweroff.refused.*`, both languages).
- **R-999 — „Kiszolgáló újraindítása" said „the whole server".** It restarts the guest. Now „A Felhom újraindítása" /
"Restart Felhom": „Újraindítja a Felhom részt a gépen: a vezérlőpultot és az összes alkalmazást. A gép maga bekapcsolva
marad. Kb. 30 másodpercig semmi nem érhető el, utána minden magától visszajön. …" — the confirmation too.
- **R-983 (one of seven):** `/api/server/reboot` acted on a GET (CsrfProtect lets GET through) — now 405.
- Tests (`internal/web/poweroff_handlers_test.go`): the word is required (5 wrong bodies, 5 right ones), POST only, the
agent's and the controller's refusals shown and nothing asked of the agent, the card hidden for an old agent (and a 404
answered as outdated), every refusal reason has a sentence in both languages, the restart route refuses a GET.
Red-proofed: each guard removed in turn convicted its test. The four `settings_system` i18n parity fixtures were
regenerated on purpose; the diff is exactly the new card, the dialog's type-to-confirm and the restart texts.
## v0.309.0 — the simpler notification page: three group switches (R-388 slice 1); the fill warning in the household's language (R-521 F7) (2026-10-12) ## v0.309.0 — the simpler notification page: three group switches (R-388 slice 1); the fill warning in the household's language (R-521 F7) (2026-10-12)
**MinAgent: 0.131.0** (unchanged — no agent call changed). **No wire change, no hub change:** the page still saves the **MinAgent: 0.131.0** (unchanged — no agent call changed). **No wire change, no hub change:** the page still saves the
+15
View File
@@ -3282,6 +3282,21 @@ restart no longer signs the household out. Expiry is unchanged (7 days). Logout
on disk too; rows written under another password are dropped at load; a revoking save that fails removes the file. on disk too; rows written under another password are dropped at load; a revoking save that fails removes the file.
Pinned by `internal/web/session_store_test.go` (`TestR35_*`). Pinned by `internal/web/session_store_test.go` (`TestR35_*`).
### 8a. Turning the machine off — „A gép kikapcsolása" (v0.310.0, R-998)
Beállítások → Rendszer, after the restart cards. Dashboard login only (`RequireAuth` + CSRF; family members' app logins
never reach the dashboard). The household types **KIKAPCSOLÁS** / **TURN OFF** (the server also takes the unaccented
spelling); `POST /api/server/poweroff {"confirm": "…"}` → the agent's `POST /host/poweroff` (agent ≥ 0.155.0, the
`host_poweroff` feature row). The agent shuts this guest down, then powers the host off; switching it on again needs the
machine's power button — the card says so. Refusals come back as one household sentence: a backup/restore/update the
controller sees itself, or the agent's reason (`too_soon_after_boot`, `already_requested_this_boot`, `backup_running`,
`other_guests_running`, …). An agent known to be older hides the button and shows one line. The hub holds its silence
alarms for 14 days (hub v0.149.0). Design: felhom.eu `documentation/architecture/14-host-power-off.md`. Tests:
`internal/web/poweroff_handlers_test.go`.
**„A Felhom újraindítása"** (was „Kiszolgáló újraindítása", R-999) restarts the guest — the dashboard and the apps —
not the machine; its text now says so. `/api/server/reboot` is POST only (R-983, one of its seven).
### 9. Central Hub Reporting ### 9. Central Hub Reporting
#### Operator actions (`internal/report/opactions.go`, `09` §3 decision 185) #### Operator actions (`internal/report/opactions.go`, `09` §3 decision 185)
+3
View File
@@ -2156,6 +2156,9 @@ func main() {
// Standalone full-server (guest) restart — the "Kiszolgáló újraindítása" maintenance affordance, // Standalone full-server (guest) restart — the "Kiszolgáló újraindítása" maintenance affordance,
// a sibling to the controller-only /api/selfrestart. Reuses the agent GuestReboot primitive. // a sibling to the controller-only /api/selfrestart. Reuses the agent GuestReboot primitive.
mux.Handle("/api/server/reboot", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.HandleServerReboot)))) mux.Handle("/api/server/reboot", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.HandleServerReboot))))
// R-998: „A gép kikapcsolása" — the household turns the whole machine off (agent POST /host/poweroff, doc 14).
// Dashboard login only (RequireAuth): family members' app logins never reach the dashboard.
mux.Handle("/api/server/poweroff", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.HandleServerPowerOff))))
// Whole-guest (appliance) backup visibility + manual trigger. Distinct prefix from apiRouter's // Whole-guest (appliance) backup visibility + manual trigger. Distinct prefix from apiRouter's
// app-data /api/backup/{run,status} (DB dumps) to avoid shadowing the /api/ catch-all subtree. // app-data /api/backup/{run,status} (DB dumps) to avoid shadowing the /api/ catch-all subtree.
mux.Handle("/api/guest-backup/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeBackupAPI)))) mux.Handle("/api/guest-backup/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeBackupAPI))))
+12
View File
@@ -83,6 +83,11 @@ const FeatureRecoveryFailureClass Feature = "recovery_failure_class"
// for two months, and it heals itself when the agent updates. // for two months, and it heals itself when the agent updates.
const FeatureRetainedRecoveryClass Feature = "retained_recovery_class" const FeatureRetainedRecoveryClass Feature = "retained_recovery_class"
// FeatureHostPowerOff is the household power-off (R-998, agent v0.155.0): POST /host/poweroff. A POST that switches
// the machine off cannot be probed, so the VERSION decides. The settings card hides only on a definite SupportNo; on
// Unknown it stays, and an old agent's 404 is answered with the update sentence.
const FeatureHostPowerOff Feature = "host_poweroff"
// SupportState is a probe verdict. The zero value is SupportUnknown (fail-open: unknown never // SupportState is a probe verdict. The zero value is SupportUnknown (fail-open: unknown never
// refuses — the existing agent-error paths speak honestly when the agent is down). // refuses — the existing agent-error paths speak honestly when the agent is down).
type SupportState int type SupportState int
@@ -159,6 +164,10 @@ var featureProbes = map[Feature]func(ctx context.Context, p SupportProber) error
FeatureRetainedRecoveryClass: func(ctx context.Context, p SupportProber) error { FeatureRetainedRecoveryClass: func(ctx context.Context, p SupportProber) error {
return errNoRecoveryProbe return errNoRecoveryProbe
}, },
// R-998: probing would switch the machine off. The row must exist (table-gap rule above).
FeatureHostPowerOff: func(ctx context.Context, p SupportProber) error {
return errNoRecoveryProbe
},
} }
// errNoMemoryProbe classifies to SupportUnknown (not a *StatusError 404), so a prober that cannot be // errNoMemoryProbe classifies to SupportUnknown (not a *StatusError 404), so a prober that cannot be
@@ -192,6 +201,9 @@ var featureMinAgent = map[Feature]string{
// screen must keep saying it cannot tell the two apart — which was true, and is what this gate // screen must keep saying it cannot tell the two apart — which was true, and is what this gate
// preserves for boxes that have not updated yet. // preserves for boxes that have not updated yet.
FeatureRetainedRecoveryClass: "0.129.0", FeatureRetainedRecoveryClass: "0.129.0",
// R-998 — POST /host/poweroff, the household's „A gép kikapcsolása".
FeatureHostPowerOff: "0.155.0",
} }
// MinAgentFor returns the declared minimum agent version for a feature ("" when the feature has no // MinAgentFor returns the declared minimum agent version for a feature ("" when the feature has no
@@ -0,0 +1,90 @@
package agentapi
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/logx"
)
// ── POST /host/poweroff (R-998, agent >= 0.155.0, felhom.eu documentation/architecture/14-host-power-off.md) ──
//
// The household's „A gép kikapcsolása". The agent answers 202 (it shuts this guest down, then the host) or 409
// with a machine-readable reason. Unlike post(), the 409 BODY matters here: the dashboard tells the household
// WHY (a backup runs, too soon after start, already asked…), so this call reads it.
// Agent refusal reasons (stable wire values; mirror felhom-agent internal/localapi/hostpoweroff.go).
const (
PowerOffTooSoon = "too_soon_after_boot"
PowerOffAlreadyAsked = "already_requested_this_boot"
PowerOffBackup = "backup_running"
PowerOffRestoreTest = "restore_test_running"
PowerOffOSUpdate = "os_update_running"
PowerOffCtrlUpdate = "controller_update_running"
PowerOffDrive = "drive_operation_running"
PowerOffEscrow = "escrow_ceremony_running"
PowerOffBusy = "host_busy"
PowerOffOtherGuests = "other_guests_running"
PowerOffUnknown = "state_unknown"
)
// PowerOffRefused is the agent's 409: Reason is one of the constants above (or a newer one), Detail is for the log.
type PowerOffRefused struct {
Reason string
Detail string
}
func (e *PowerOffRefused) Error() string {
return "agent refused the power-off: " + e.Reason + " (" + e.Detail + ")"
}
// HostPowerOff asks the agent to shut this guest down and power the host off. nil = accepted (the box goes down
// within minutes). A *PowerOffRefused = the agent said no, with its reason. A *StatusError 404 = an agent that
// predates the route.
func (c *Client) HostPowerOff(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/host/poweroff", bytes.NewReader([]byte("{}")))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", "application/json")
start := time.Now()
resp, err := c.hc.Do(req)
if err != nil {
return fmt.Errorf("agentapi: POST /host/poweroff: %w", err)
}
defer resp.Body.Close()
c.noteAgentVersion(resp)
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
logx.Debugf(c.logger, "[agentapi] POST /host/poweroff -> %d (%dms)", resp.StatusCode, time.Since(start).Milliseconds())
switch resp.StatusCode {
case http.StatusAccepted, http.StatusOK:
return nil
case http.StatusConflict:
var env struct {
Data struct {
Reason string `json:"reason"`
Detail string `json:"detail"`
} `json:"data"`
}
if json.Unmarshal(raw, &env) != nil || env.Data.Reason == "" {
return &PowerOffRefused{Reason: PowerOffUnknown, Detail: "unreadable 409 body"}
}
return &PowerOffRefused{Reason: env.Data.Reason, Detail: env.Data.Detail}
default:
return &StatusError{Method: http.MethodPost, Path: "/host/poweroff", Code: resp.StatusCode}
}
}
// IsPowerOffRefused returns the refusal, if err is one.
func IsPowerOffRefused(err error) (*PowerOffRefused, bool) {
var r *PowerOffRefused
ok := errors.As(err, &r)
return r, ok
}
+23 -4
View File
@@ -2031,13 +2031,13 @@
"settings_system.atmeretezes": "Resize", "settings_system.atmeretezes": "Resize",
"settings_system.atmeretezes_2": "Resizing...", "settings_system.atmeretezes_2": "Resizing...",
"settings_system.automatikus_frissites": "Automatic update", "settings_system.automatikus_frissites": "Automatic update",
"settings_system.az_egesz_kiszolgalo_szerver_ujrainditasa": "Restarts the whole server. Every app stops for a short time, and the dashboard is unreachable for about 30 seconds. Use it when the whole system needs a restart — otherwise “Restart controller” above is enough.", "settings_system.az_egesz_kiszolgalo_szerver_ujrainditasa": "Restarts the Felhom part of the machine: the dashboard and all your apps. The machine itself stays on. Nothing can be reached for about 30 seconds; then everything comes back by itself. If only the dashboard is stuck, “Restart controller” above is enough.",
"settings_system.az_ujrainditas_a_vartnal_tovabb": "The restart is taking longer than expected. Reload the page by hand.", "settings_system.az_ujrainditas_a_vartnal_tovabb": "The restart is taking longer than expected. Reload the page by hand.",
"settings_system.az_uzemelteto_altal_beallitott_ertekek": "Values set by the operator. Ask the operator to change them.", "settings_system.az_uzemelteto_altal_beallitott_ertekek": "Values set by the operator. Ask the operator to change them.",
"settings_system.beallitasok_rendszer": "Settings — System", "settings_system.beallitasok_rendszer": "Settings — System",
"settings_system.biztonsagi_mentes": "Backup", "settings_system.biztonsagi_mentes": "Backup",
"settings_system.biztosan_frissiti_a_controllert_n": "Update the controller?\\n\\nThe dashboard is unreachable for a short time during the update.", "settings_system.biztosan_frissiti_a_controllert_n": "Update the controller?\\n\\nThe dashboard is unreachable for a short time during the update.",
"settings_system.biztosan_ujrainditja_a_kiszolgalot_az": "Restart the server? The apps and the dashboard become unreachable for about 30 seconds.", "settings_system.biztosan_ujrainditja_a_kiszolgalot_az": "Restart Felhom? The apps and the dashboard become unreachable for about 30 seconds. The machine stays on.",
"settings_system.biztosan_ujrainditja_a_vezerlot_a": "Restart the controller? It takes a few seconds, and the page is unreachable for a short time.", "settings_system.biztosan_ujrainditja_a_vezerlot_a": "Restart the controller? It takes a few seconds, and the page is unreachable for a short time.",
"settings_system.csokkentes": "Reduce", "settings_system.csokkentes": "Reduce",
"settings_system.ellenorzes": "Checking...", "settings_system.ellenorzes": "Checking...",
@@ -2063,7 +2063,7 @@
"settings_system.jelenlegi_verzio": "Current version", "settings_system.jelenlegi_verzio": "Current version",
"settings_system.kapcsolodasi_hiba": "Connection error", "settings_system.kapcsolodasi_hiba": "Connection error",
"settings_system.kesz": "Done.", "settings_system.kesz": "Done.",
"settings_system.kiszolgalo_ujrainditasa": "Restart server", "settings_system.kiszolgalo_ujrainditasa": "Restart Felhom",
"settings_system.legujabb_verzio": "Latest version", "settings_system.legujabb_verzio": "Latest version",
"settings_system.megengedett_tartomany": "Allowed range", "settings_system.megengedett_tartomany": "Allowed range",
"settings_system.megerosites": "Confirm", "settings_system.megerosites": "Confirm",
@@ -2666,5 +2666,24 @@
"family_gate.confirm_reset": "Give a new password? The old one stops at once.", "family_gate.confirm_reset": "Give a new password? The old one stops at once.",
"family_gate.error": "That did not work. Try again.", "family_gate.error": "That did not work. Try again.",
"waiting.body": "This machine has just started for the first time, but it could not download its settings from the Felhom central service yet. It tries again every minute; once it succeeds, this page turns into your dashboard by itself. If you still see this in an hour, write to us: support@felhom.eu", "waiting.body": "This machine has just started for the first time, but it could not download its settings from the Felhom central service yet. It tries again every minute; once it succeeds, this page turns into your dashboard by itself. If you still see this in an hour, write to us: support@felhom.eu",
"waiting.title": "Felhom has not reached the central service yet" "waiting.title": "Felhom has not reached the central service yet",
"settings_system.megerositeshez_ird_be": "To confirm, type:",
"settings_system.a_gep_kikapcsolasa": "Turn the machine off",
"settings_system.a_gep_teljesen_kikapcsol": "The machine switches off completely. Your apps and the backups stop.",
"settings_system.ujra_bekapcsolni_csak_otthon": "It can only be switched on again at home: press the power button of the machine.",
"settings_system.kikapcsolas_frissites_utan": "Turning the machine off needs an update of the software of the machine. It comes by itself; until then this button is hidden.",
"settings_system.kikapcsolas_szo": "TURN OFF",
"settings_system.kikapcsolas": "Turn off",
"settings_system.biztosan_kikapcsolod_a_gepet": "Turn the machine off? Felhom stops and the machine switches off. Nobody can switch it on again from afar: only at home, with the power button of the machine.",
"settings_system.a_gep_kikapcsol_folyamatban": "The machine is switching off. In a few minutes it stops completely, and this page will not answer any more. To switch it on again: press the power button of the machine.",
"poweroff.confirm_required": "Type the confirmation word: TURN OFF.",
"poweroff.agent_down": "The machine cannot be turned off now: the dashboard cannot reach the manager of the machine. Try again in a few minutes.",
"poweroff.agent_outdated": "Turning the machine off needs an update of the software of the machine. It comes by itself.",
"poweroff.refused.too_soon": "The machine has just started. You can turn it off in 10 minutes.",
"poweroff.refused.already": "The machine is already switching off.",
"poweroff.refused.backup": "A backup or a restore is running. Wait until it ends, then try again.",
"poweroff.refused.update": "An update is running. Wait until it ends, then try again.",
"poweroff.refused.drive": "The machine is working on a drive. Wait until it ends, then try again.",
"poweroff.refused.other_guests": "Another system runs on this machine, so it cannot be turned off from here.",
"poweroff.refused.generic": "The machine cannot turn itself off safely right now. Try again in a few minutes."
} }
+23 -4
View File
@@ -2019,13 +2019,13 @@
"settings_system.atmeretezes": "Átméretezés", "settings_system.atmeretezes": "Átméretezés",
"settings_system.atmeretezes_2": "Átméretezés...", "settings_system.atmeretezes_2": "Átméretezés...",
"settings_system.automatikus_frissites": "Automatikus frissítés", "settings_system.automatikus_frissites": "Automatikus frissítés",
"settings_system.az_egesz_kiszolgalo_szerver_ujrainditasa": "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áld, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő.", "settings_system.az_egesz_kiszolgalo_szerver_ujrainditasa": "Újraindítja a Felhom részt a gépen: a vezérlőpultot és az összes alkalmazást. A gép maga bekapcsolva marad. Kb. 30 másodpercig semmi nem érhető el, utána minden magától visszajön. Ha csak a vezérlőpult akad, a fenti „Vezérlő újraindítása” elég.",
"settings_system.az_ujrainditas_a_vartnal_tovabb": "Az újraindítás a vártnál tovább tart. Töltsd újra az oldalt kézzel.", "settings_system.az_ujrainditas_a_vartnal_tovabb": "Az újraindítás a vártnál tovább tart. Töltsd újra az oldalt kézzel.",
"settings_system.az_uzemelteto_altal_beallitott_ertekek": "Az üzemeltető által beállított értékek. Módosításhoz kérd az üzemeltetőt.", "settings_system.az_uzemelteto_altal_beallitott_ertekek": "Az üzemeltető által beállított értékek. Módosításhoz kérd az üzemeltetőt.",
"settings_system.beallitasok_rendszer": "Beállítások — Rendszer", "settings_system.beallitasok_rendszer": "Beállítások — Rendszer",
"settings_system.biztonsagi_mentes": "Biztonsági mentés", "settings_system.biztonsagi_mentes": "Biztonsági mentés",
"settings_system.biztosan_frissiti_a_controllert_n": "Biztosan frissíti a controllert?\\n\\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.", "settings_system.biztosan_frissiti_a_controllert_n": "Biztosan frissíti a controllert?\\n\\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.",
"settings_system.biztosan_ujrainditja_a_kiszolgalot_az": "Biztosan újraindítod a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.", "settings_system.biztosan_ujrainditja_a_kiszolgalot_az": "Biztosan újraindítod a Felhomot? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak. A gép bekapcsolva marad.",
"settings_system.biztosan_ujrainditja_a_vezerlot_a": "Biztosan újraindítod a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.", "settings_system.biztosan_ujrainditja_a_vezerlot_a": "Biztosan újraindítod a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.",
"settings_system.csokkentes": "Csökkentés", "settings_system.csokkentes": "Csökkentés",
"settings_system.ellenorzes": "Ellenőrzés...", "settings_system.ellenorzes": "Ellenőrzés...",
@@ -2051,7 +2051,7 @@
"settings_system.jelenlegi_verzio": "Jelenlegi verzió", "settings_system.jelenlegi_verzio": "Jelenlegi verzió",
"settings_system.kapcsolodasi_hiba": "Kapcsolódási hiba", "settings_system.kapcsolodasi_hiba": "Kapcsolódási hiba",
"settings_system.kesz": "Kész.", "settings_system.kesz": "Kész.",
"settings_system.kiszolgalo_ujrainditasa": "Kiszolgáló újraindítása", "settings_system.kiszolgalo_ujrainditasa": "A Felhom újraindítása",
"settings_system.legujabb_verzio": "Legújabb verzió", "settings_system.legujabb_verzio": "Legújabb verzió",
"settings_system.megengedett_tartomany": "Megengedett tartomány", "settings_system.megengedett_tartomany": "Megengedett tartomány",
"settings_system.megerosites": "Megerősítés", "settings_system.megerosites": "Megerősítés",
@@ -2654,5 +2654,24 @@
"family_gate.confirm_reset": "Új jelszót adsz? A régi azonnal megszűnik.", "family_gate.confirm_reset": "Új jelszót adsz? A régi azonnal megszűnik.",
"family_gate.error": "Nem sikerült. Próbáld újra.", "family_gate.error": "Nem sikerült. Próbáld újra.",
"waiting.body": "Ez a gép most indult el először, de még nem tudta letölteni a beállításait a Felhom központi rendszeréből. Percenként újrapróbálkozik; amint sikerül, ez az oldal magától a vezérlőpultra vált. Ha egy óra múlva is ezt látod, írj nekünk: support@felhom.eu", "waiting.body": "Ez a gép most indult el először, de még nem tudta letölteni a beállításait a Felhom központi rendszeréből. Percenként újrapróbálkozik; amint sikerül, ez az oldal magától a vezérlőpultra vált. Ha egy óra múlva is ezt látod, írj nekünk: support@felhom.eu",
"waiting.title": "A Felhom még nem érte el a központot" "waiting.title": "A Felhom még nem érte el a központot",
"settings_system.megerositeshez_ird_be": "Megerősítéshez írd be:",
"settings_system.a_gep_kikapcsolasa": "A gép kikapcsolása",
"settings_system.a_gep_teljesen_kikapcsol": "A gép teljesen kikapcsol. Az alkalmazásaid és a mentések leállnak.",
"settings_system.ujra_bekapcsolni_csak_otthon": "Újra bekapcsolni csak otthon lehet: nyomd meg a gép bekapcsológombját.",
"settings_system.kikapcsolas_frissites_utan": "A gép kikapcsolásához a gép szoftverének frissítése kell. Ez magától megtörténik, addig ez a gomb nem látszik.",
"settings_system.kikapcsolas_szo": "KIKAPCSOLÁS",
"settings_system.kikapcsolas": "Kikapcsolás",
"settings_system.biztosan_kikapcsolod_a_gepet": "Biztosan kikapcsolod a gépet? A Felhom leáll, és a gép kikapcsol. Távolról senki nem tudja újra bekapcsolni: csak otthon, a gép bekapcsológombjával.",
"settings_system.a_gep_kikapcsol_folyamatban": "A gép kikapcsol. Néhány perc múlva teljesen leáll, és ez az oldal nem válaszol többé. Újra bekapcsolni: nyomd meg a gép bekapcsológombját.",
"poweroff.confirm_required": "Írd be a megerősítő szót: KIKAPCSOLÁS.",
"poweroff.agent_down": "A gépet most nem lehet kikapcsolni, mert a vezérlő nem éri el a gép kezelőjét. Próbáld újra néhány perc múlva.",
"poweroff.agent_outdated": "A gép kikapcsolásához a gép szoftverének frissítése kell. Ez magától megtörténik.",
"poweroff.refused.too_soon": "A gép most indult el. 10 perc múlva kapcsolhatod ki.",
"poweroff.refused.already": "A kikapcsolás már folyamatban van.",
"poweroff.refused.backup": "Most mentés vagy visszaállítás fut. Várd meg, amíg befejeződik, és próbáld újra.",
"poweroff.refused.update": "Most frissítés fut. Várd meg, amíg befejeződik, és próbáld újra.",
"poweroff.refused.drive": "A gép most egy meghajtón dolgozik. Várd meg, amíg befejeződik, és próbáld újra.",
"poweroff.refused.other_guests": "Ezen a gépen más rendszer is fut, ezért innen nem lehet kikapcsolni.",
"poweroff.refused.generic": "A gép most nem tudja biztonságosan kikapcsolni magát. Próbáld újra néhány perc múlva."
} }
+1
View File
@@ -2346,6 +2346,7 @@ func (s *Server) systemPageData() map[string]interface{} {
} }
// Guest RAM resize card (v0.143.0, R-24): current allocation + bounds + capability/reachability. // Guest RAM resize card (v0.143.0, R-24): current allocation + bounds + capability/reachability.
s.memoryCardData(data) s.memoryCardData(data)
s.powerOffCardData(data) // R-998: hide „A gép kikapcsolása" only for a definitely-too-old agent
// „Hálózat" card (R-66): where the box IS, live-computed per render and stored NOWHERE — the // „Hálózat" card (R-66): where the box IS, live-computed per render and stored NOWHERE — the
// guest holds its address by DHCP, so a stored copy eventually misdirects people (S-5); an // guest holds its address by DHCP, so a stored copy eventually misdirects people (S-5); an
@@ -213,6 +213,7 @@ func i18nCasesA() []i18nCase {
d["NetLANAddress"], d["NetSMBName"], d["NetGateway"] = "192.168.1.10", "FELHOM", "192.168.1.1" d["NetLANAddress"], d["NetSMBName"], d["NetGateway"] = "192.168.1.10", "FELHOM", "192.168.1.1"
d["MemorySupported"], d["MemoryReachable"] = true, true d["MemorySupported"], d["MemoryReachable"] = true, true
d["MemoryAllocatedMB"], d["MemoryUsageMB"], d["MemoryMinMB"], d["MemoryMaxMB"] = 4096, 2048, 2048, 8192 d["MemoryAllocatedMB"], d["MemoryUsageMB"], d["MemoryMinMB"], d["MemoryMaxMB"] = 4096, 2048, 2048, 8192
d["PowerOffSupported"] = true // R-998: the power-off card with its button
return d return d
}}, }},
{"settings_system_failed", "settings_system", func() map[string]interface{} { {"settings_system_failed", "settings_system", func() map[string]interface{} {
@@ -223,6 +224,7 @@ func i18nCasesA() []i18nCase {
d["RegistryAnonymous"] = false d["RegistryAnonymous"] = false
d["LastUpdateState"] = map[string]interface{}{"Status": "failed", "Error": "pull failed"} d["LastUpdateState"] = map[string]interface{}{"Status": "failed", "Error": "pull failed"}
d["MemorySupported"], d["MemoryReachable"], d["MemoryProcMB"] = true, false, 4096 d["MemorySupported"], d["MemoryReachable"], d["MemoryProcMB"] = true, false, 4096
d["PowerOffSupported"] = true
return d return d
}}, }},
{"settings_system_pending", "settings_system", func() map[string]interface{} { {"settings_system_pending", "settings_system", func() map[string]interface{} {
@@ -231,11 +233,13 @@ func i18nCasesA() []i18nCase {
d["SelfUpdateEnabled"] = true d["SelfUpdateEnabled"] = true
d["LastUpdateState"] = map[string]interface{}{"Status": "pending"} d["LastUpdateState"] = map[string]interface{}{"Status": "pending"}
d["MemorySupported"], d["MemoryReachable"], d["MemoryProcMB"] = true, false, 0 d["MemorySupported"], d["MemoryReachable"], d["MemoryProcMB"] = true, false, 0
d["PowerOffSupported"] = true
return d return d
}}, }},
{"settings_system_nomemory", "settings_system", func() map[string]interface{} { {"settings_system_nomemory", "settings_system", func() map[string]interface{} {
d := i18nLayoutData("settings", "Rendszer beállítások") d := i18nLayoutData("settings", "Rendszer beállítások")
d["MemorySupported"] = false d["MemorySupported"] = false
d["PowerOffSupported"] = false // R-998: an old agent — the card's one-line note instead of the button
return d return d
}}, }},
{"settings_security_full", "settings_security", func() map[string]interface{} { {"settings_security_full", "settings_security", func() map[string]interface{} {
@@ -0,0 +1,142 @@
package web
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
)
// ── „A gép kikapcsolása" — the household turns the whole machine off (R-998) ─────────────────────────────
//
// Design: felhom.eu documentation/architecture/14-host-power-off.md. The controller is the front door only; the
// agent's POST /host/poweroff holds the real guards (10 min after boot, one per boot, no other guest, no job).
// Here: dashboard login (RequireAuth + CSRF, wired in main.go), POST only, the typed confirmation word, and the
// controller's OWN busy work (a backup/restore/app update the agent cannot see). Pinned by poweroff_handlers_test.go.
// powerOffAgent is the agent seam: the call, and the version gate.
type powerOffAgent interface {
HostPowerOff(ctx context.Context) error
Supports(ctx context.Context, f agentapi.Feature) agentapi.SupportState
}
// powerOffConfirmWords are what the household may type. The unaccented spelling is accepted on purpose: a phone or
// a foreign keyboard may not have „Á", and the word is a deliberate act, not a password.
var powerOffConfirmWords = map[string]bool{"KIKAPCSOLÁS": true, "KIKAPCSOLAS": true, "TURN OFF": true}
func powerOffConfirmed(typed string) bool {
return powerOffConfirmWords[strings.ToUpper(strings.Join(strings.Fields(typed), " "))]
}
func (s *Server) powerOffAgentFor() (powerOffAgent, error) {
if s.powerOffAgentFn != nil {
return s.powerOffAgentFn()
}
c, err := s.agentClient()
if err != nil {
return nil, err
}
return c, nil
}
// powerOffLocalBusy is the controller-side work the agent cannot see.
func (s *Server) powerOffLocalBusy() (bool, string) {
if s.powerOffBusyFn != nil {
return s.powerOffBusyFn()
}
if s.backupMgr != nil && (s.backupMgr.IsRunning() || s.backupMgr.RestoreStatus().Running) {
return true, "a backup or restore is running"
}
if s.stackMgr != nil && s.stackMgr.AnyUpdating() {
return true, "an app update is running"
}
return false, ""
}
// powerOffCardData fills the settings card: hidden (with a note) only on a DEFINITE "agent too old".
func (s *Server) powerOffCardData(data map[string]interface{}) {
data["PowerOffSupported"] = true
agent, err := s.powerOffAgentFor()
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if agent.Supports(ctx, agentapi.FeatureHostPowerOff) == agentapi.SupportNo {
data["PowerOffSupported"] = false
}
}
// powerOffRefusalKey maps the agent's reason to the household sentence.
func powerOffRefusalKey(reason string) string {
switch reason {
case agentapi.PowerOffTooSoon:
return "poweroff.refused.too_soon"
case agentapi.PowerOffAlreadyAsked:
return "poweroff.refused.already"
case agentapi.PowerOffBackup, agentapi.PowerOffRestoreTest:
return "poweroff.refused.backup"
case agentapi.PowerOffOSUpdate, agentapi.PowerOffCtrlUpdate:
return "poweroff.refused.update"
case agentapi.PowerOffDrive:
return "poweroff.refused.drive"
case agentapi.PowerOffOtherGuests:
return "poweroff.refused.other_guests"
default:
return "poweroff.refused.generic"
}
}
// HandleServerPowerOff is POST /api/server/poweroff {"confirm": "<the word>"}.
func (s *Server) HandleServerPowerOff(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
writeDiskJSON(w, http.StatusMethodNotAllowed, false, "POST only", nil)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 4<<10)
var req struct {
Confirm string `json:"confirm"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if !powerOffConfirmed(req.Confirm) {
writeDiskJSON(w, http.StatusBadRequest, false, s.msg(r, "poweroff.confirm_required"), map[string]any{"code": "confirm_required"})
return
}
if busy, why := s.powerOffLocalBusy(); busy {
s.logger.Printf("[INFO] [web] power-off refused: %s", why)
writeDiskJSON(w, http.StatusConflict, false, s.msg(r, "poweroff.refused.backup"), map[string]any{"code": "controller_busy"})
return
}
agent, err := s.powerOffAgentFor()
if err != nil {
writeDiskJSON(w, http.StatusServiceUnavailable, false, s.msg(r, "poweroff.agent_down"), nil)
return
}
if agent.Supports(r.Context(), agentapi.FeatureHostPowerOff) == agentapi.SupportNo {
writeDiskJSON(w, http.StatusPreconditionFailed, false, s.msg(r, "poweroff.agent_outdated"), map[string]any{"code": "agent_outdated"})
return
}
err = agent.HostPowerOff(r.Context())
if ref, ok := agentapi.IsPowerOffRefused(err); ok {
s.logger.Printf("[INFO] [web] power-off refused by the agent: %s (%s)", ref.Reason, ref.Detail)
writeDiskJSON(w, http.StatusConflict, false, s.msg(r, powerOffRefusalKey(ref.Reason)), map[string]any{"code": ref.Reason})
return
}
var se *agentapi.StatusError
if errors.As(err, &se) && se.Code == http.StatusNotFound {
writeDiskJSON(w, http.StatusPreconditionFailed, false, s.msg(r, "poweroff.agent_outdated"), map[string]any{"code": "agent_outdated"})
return
}
if err != nil {
s.logger.Printf("[ERROR] [web] power-off request failed: %v", err)
writeDiskJSON(w, http.StatusBadGateway, false, s.msg(r, "poweroff.agent_down"), nil)
return
}
s.logger.Printf("[WARN] [web] MACHINE POWER-OFF accepted (household request from the dashboard)")
writeDiskJSON(w, http.StatusAccepted, true, "", map[string]any{"powering_off": true})
}
@@ -0,0 +1,144 @@
package web
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
)
// R-998 — „A gép kikapcsolása". The consequence is asserted: was the agent asked to switch the machine off, or not?
type fakePowerAgent struct {
support agentapi.SupportState
err error
calls int
}
func (f *fakePowerAgent) HostPowerOff(context.Context) error { f.calls++; return f.err }
func (f *fakePowerAgent) Supports(context.Context, agentapi.Feature) agentapi.SupportState {
return f.support
}
func powerServer(t *testing.T, a *fakePowerAgent) *Server {
t.Helper()
s := testPageServer(t)
s.powerOffAgentFn = func() (powerOffAgent, error) { return a, nil }
s.powerOffBusyFn = func() (bool, string) { return false, "" }
return s
}
func postPowerOff(s *Server, method, body string) *httptest.ResponseRecorder {
r := httptest.NewRequest(method, "/api/server/poweroff", strings.NewReader(body))
w := httptest.NewRecorder()
s.HandleServerPowerOff(w, r)
return w
}
// Without the typed word nothing is switched off; with it (either language, either spelling) the agent is asked once.
func TestPowerOff_ConfirmationWordRequired(t *testing.T) {
for _, body := range []string{``, `{}`, `{"confirm":""}`, `{"confirm":"igen"}`, `{"confirm":"KIKAPCS"}`} {
a := &fakePowerAgent{support: agentapi.SupportYes}
w := postPowerOff(powerServer(t, a), http.MethodPost, body)
if w.Code != http.StatusBadRequest || a.calls != 0 {
t.Fatalf("body %q: got %d, agent asked %d times — want 400 and no call", body, w.Code, a.calls)
}
if !strings.Contains(w.Body.String(), "KIKAPCSOLÁS") {
t.Fatalf("the refusal does not name the word: %s", w.Body.String())
}
}
for _, word := range []string{"KIKAPCSOLÁS", "kikapcsolás", " KIKAPCSOLAS ", "TURN OFF", "turn off"} {
a := &fakePowerAgent{support: agentapi.SupportYes}
w := postPowerOff(powerServer(t, a), http.MethodPost, `{"confirm":"`+word+`"}`)
if w.Code != http.StatusAccepted || a.calls != 1 {
t.Fatalf("word %q: got %d (%s), calls %d — want 202 and one call", word, w.Code, w.Body.String(), a.calls)
}
}
}
// POST only: a GET (a link, an image tag) must never switch the machine off.
func TestPowerOff_PostOnly(t *testing.T) {
a := &fakePowerAgent{support: agentapi.SupportYes}
w := postPowerOff(powerServer(t, a), http.MethodGet, `{"confirm":"KIKAPCSOLÁS"}`)
if w.Code != http.StatusMethodNotAllowed || a.calls != 0 {
t.Fatalf("GET: %d, calls %d", w.Code, a.calls)
}
}
// The agent's refusal reaches the household as a sentence; the controller's own busy work refuses before the agent.
func TestPowerOff_RefusalsShown(t *testing.T) {
a := &fakePowerAgent{support: agentapi.SupportYes, err: &agentapi.PowerOffRefused{Reason: agentapi.PowerOffBackup}}
w := postPowerOff(powerServer(t, a), http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`)
if w.Code != http.StatusConflict || !strings.Contains(w.Body.String(), "Most mentés vagy visszaállítás fut") ||
!strings.Contains(w.Body.String(), `"code":"backup_running"`) {
t.Fatalf("agent refusal: %d %s", w.Code, w.Body.String())
}
a2 := &fakePowerAgent{support: agentapi.SupportYes}
s := powerServer(t, a2)
s.powerOffBusyFn = func() (bool, string) { return true, "a backup or restore is running" }
if w := postPowerOff(s, http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`); w.Code != http.StatusConflict || a2.calls != 0 {
t.Fatalf("controller busy: %d, agent calls %d — want 409 and no call", w.Code, a2.calls)
}
}
// An agent older than the route: the card is HIDDEN with a one-line note, and the route answers agent_outdated
// without asking the agent. An agent that 404s anyway (version unknown) gets the same answer.
func TestPowerOff_HiddenForOldAgent(t *testing.T) {
old := &fakePowerAgent{support: agentapi.SupportNo}
s := powerServer(t, old)
page := getPage(t, s, "/settings").Body.String()
if strings.Contains(page, `id="btn-poweroff"`) || !strings.Contains(page, "data-poweroff-unavailable") {
t.Fatal("an old agent: the power-off button must be hidden, with the note")
}
if w := postPowerOff(s, http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`); w.Code != http.StatusPreconditionFailed || old.calls != 0 {
t.Fatalf("old agent: %d, calls %d", w.Code, old.calls)
}
cur := &fakePowerAgent{support: agentapi.SupportYes}
page = getPage(t, powerServer(t, cur), "/settings").Body.String()
if !strings.Contains(page, `id="btn-poweroff"`) || !strings.Contains(page, "A gép kikapcsolása") ||
!strings.Contains(page, "nyomd meg a gép bekapcsológombját") {
t.Fatal("a current agent: the card, its title and the power-button sentence must be on the page")
}
unknown := &fakePowerAgent{support: agentapi.SupportUnknown, err: &agentapi.StatusError{Method: "POST", Path: "/host/poweroff", Code: 404}}
if w := postPowerOff(powerServer(t, unknown), http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`); w.Code != http.StatusPreconditionFailed {
t.Fatalf("404 from an unknown-version agent: %d, want 412", w.Code)
}
}
// R-983 (one of seven): the restart route must not act on a GET.
func TestServerReboot_PostOnly(t *testing.T) {
s := testServer(t)
w := httptest.NewRecorder()
s.HandleServerReboot(w, httptest.NewRequest(http.MethodGet, "/api/server/reboot", nil))
if w.Code != http.StatusMethodNotAllowed {
t.Fatalf("GET /api/server/reboot = %d, want 405", w.Code)
}
}
// Every agent refusal reason maps to a household sentence that exists in BOTH languages (an unknown reason → the
// generic one), and an unreachable agent answers with its own sentence and asks nobody to switch anything off.
func TestPowerOff_EveryReasonHasASentence(t *testing.T) {
s := testPageServer(t)
b := s.bundle()
for _, reason := range []string{agentapi.PowerOffTooSoon, agentapi.PowerOffAlreadyAsked, agentapi.PowerOffBackup,
agentapi.PowerOffRestoreTest, agentapi.PowerOffOSUpdate, agentapi.PowerOffCtrlUpdate, agentapi.PowerOffDrive,
agentapi.PowerOffEscrow, agentapi.PowerOffBusy, agentapi.PowerOffOtherGuests, agentapi.PowerOffUnknown, "a_reason_from_the_future"} {
key := powerOffRefusalKey(reason)
for _, lang := range []string{"hu", "en"} {
if got := b.Msg(lang, key); got == "" || got == key {
t.Errorf("reason %q → key %q has no %s text", reason, key, lang)
}
}
}
down := testPageServer(t)
down.powerOffAgentFn = func() (powerOffAgent, error) { return nil, errors.New("agent not configured") }
down.powerOffBusyFn = func() (bool, string) { return false, "" }
w := postPowerOff(down, http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`)
if w.Code != http.StatusServiceUnavailable || !strings.Contains(w.Body.String(), "nem éri el a gép kezelőjét") {
t.Fatalf("agent down: %d %s", w.Code, w.Body.String())
}
}
+4
View File
@@ -201,6 +201,10 @@ type Server struct {
memFeatures agentapi.SupportCache memFeatures agentapi.SupportCache
memAgentFn func() (memAgent, error) memAgentFn func() (memAgent, error)
// R-998: test seams for the power-off route (nil in production → the real agent client / busy checks).
powerOffAgentFn func() (powerOffAgent, error)
powerOffBusyFn func() (bool, string)
// Asset syncer for Hub-managed assets (optional) // Asset syncer for Hub-managed assets (optional)
assetsSyncer *assets.Syncer assetsSyncer *assets.Syncer
@@ -61,7 +61,7 @@ func TestSettingsSplitPagesRender(t *testing.T) {
mustNot []string mustNot []string
}{ }{
{"/settings", {"/settings",
[]string{"Rendszer konfiguráció", "Verzió és frissítés", "Vezérlő újraindítása", "Kiszolgáló újraindítása"}, []string{"Rendszer konfiguráció", "Verzió és frissítés", "Vezérlő újraindítása", "A Felhom újraindítása", "A gép kikapcsolása"},
[]string{"Adattárolók", "Jelszó módosítás", "Értesítési szünet"}}, []string{"Adattárolók", "Jelszó módosítás", "Értesítési szünet"}},
{"/settings/notifications", {"/settings/notifications",
[]string{"Beállítások — Értesítések"}, []string{"Beállítások — Értesítések"},
@@ -110,7 +110,7 @@ func TestSettingsSectionInventory(t *testing.T) {
"Alkalmazás-email", "Alkalmazás-email",
"Vészhelyzeti információk", // renamed from the misspelled "Veszhelyzeti informaciok" "Vészhelyzeti információk", // renamed from the misspelled "Veszhelyzeti informaciok"
"Vezérlő újraindítása", "Vezérlő újraindítása",
"Kiszolgáló újraindítása", "A Felhom újraindítása", // R-999: was „Kiszolgáló újraindítása" — it restarts the Felhom part, not the machine
} }
var union strings.Builder var union strings.Builder
for _, f := range []string{"settings_system.html", "settings_notifications.html", "settings_security.html", "storage.html", "storage_network.html"} { for _, f := range []string{"settings_system.html", "settings_notifications.html", "settings_security.html", "storage.html", "storage_network.html"} {
@@ -817,6 +817,12 @@ func (s *Server) handleStorageWipe(w http.ResponseWriter, r *http.Request) {
// drive-activation banner; in the intermediary-mount model a drive binds live, so no reboot is needed // drive-activation banner; in the intermediary-mount model a drive binds live, so no reboot is needed
// to activate storage — this button is purely a full-server restart.) // to activate storage — this button is purely a full-server restart.)
func (s *Server) HandleServerReboot(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleServerReboot(w http.ResponseWriter, r *http.Request) {
// R-983 (one of its seven): CsrfProtect lets a GET through, so a link could restart the guest. POST only.
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
writeDiskJSON(w, http.StatusMethodNotAllowed, false, "POST only", nil)
return
}
agent, err := s.agentClient() agent, err := s.agentClient()
if err != nil { if err != nil {
writeDiskJSON(w, http.StatusServiceUnavailable, false, s.errText(r, err), nil) writeDiskJSON(w, http.StatusServiceUnavailable, false, s.errText(r, err), nil)
@@ -240,10 +240,15 @@ function openDialog(opts){
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">' root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
+'<h3>'+escText(opts.title||'{{T "settings_system.megerosites"}}')+'</h3>' +'<h3>'+escText(opts.title||'{{T "settings_system.megerosites"}}')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>' +'<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||'{{T "settings_system.megerosites"}}')+'</button>' // R-998: typeToConfirm — the button stays off until the household types the word (the storage page's pattern).
+(opts.typeToConfirm?'<div class="confirm-input"><label>{{T "settings_system.megerositeshez_ird_be"}} <strong class="mono">'+escText(opts.typeToConfirm)+'</strong></label>'
+'<input type="text" id="dialog-type" class="form-control" autocomplete="off" placeholder="'+escText(opts.typeToConfirm)+'"></div>':'')
+'<div class="form-actions"><button id="dialog-go" class="btn '+(opts.typeToConfirm?'btn-danger-outline':'btn-primary')+'"'+(opts.typeToConfirm?' disabled':'')+'>'+escText(opts.confirmLabel||'{{T "settings_system.megerosites"}}')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">{{T "common.megsem"}}</button></div>' +'<button type="button" class="btn btn-outline" id="dialog-cancel">{{T "common.megsem"}}</button></div>'
+'</div></div>'; +'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); }; var typed=document.getElementById('dialog-type');
if(typed){ typed.oninput=function(){ document.getElementById('dialog-go').disabled=(this.value.trim().toUpperCase()!==opts.typeToConfirm.toUpperCase()); }; }
document.getElementById('dialog-go').onclick=function(){ var v=typed?typed.value:''; closeDialog(); if(opts.onConfirm) opts.onConfirm(v); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); }; document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
} }
@@ -395,6 +400,20 @@ pollUntilBack();
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">{{T "settings_system.kiszolgalo_ujrainditasa"}}</button> <button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">{{T "settings_system.kiszolgalo_ujrainditasa"}}</button>
</div> </div>
<!-- Section: R-998 — „A gép kikapcsolása": the whole machine off (agent POST /host/poweroff, doc 14). Dashboard login
only; the household types the word; the agent may still refuse (its reason is shown). Hidden for an old agent. -->
<div class="settings-card" id="poweroff-card">
<h3>{{T "settings_system.a_gep_kikapcsolasa"}}</h3>
{{if .PowerOffSupported}}
<p class="settings-card-desc">{{T "settings_system.a_gep_teljesen_kikapcsol"}}</p>
<p class="settings-card-desc"><strong>{{T "settings_system.ujra_bekapcsolni_csak_otthon"}}</strong></p>
<div id="poweroff-status"></div>
<button type="button" class="btn btn-danger-outline" id="btn-poweroff" onclick="powerOffMachine()">{{T "settings_system.a_gep_kikapcsolasa"}}</button>
{{else}}
<p class="settings-card-desc" data-poweroff-unavailable>{{T "settings_system.kikapcsolas_frissites_utan"}}</p>
{{end}}
</div>
<script> <script>
function restartController() { function restartController() {
openDialog({title:'{{T "settings_system.vezerlo_ujrainditasa"}}', confirmLabel:'{{T "common.ujrainditas"}}', openDialog({title:'{{T "settings_system.vezerlo_ujrainditasa"}}', confirmLabel:'{{T "common.ujrainditas"}}',
@@ -424,6 +443,32 @@ function doRestartServer() {
.then(function(){ pollRestart(0); }) .then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless .catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless
} }
function powerOffMachine() {
var word = '{{T "settings_system.kikapcsolas_szo"}}';
openDialog({title:'{{T "settings_system.a_gep_kikapcsolasa"}}', confirmLabel:'{{T "settings_system.kikapcsolas"}}',
message:'{{T "settings_system.biztosan_kikapcsolod_a_gepet"}}', typeToConfirm: word,
onConfirm:doPowerOffMachine});
}
function doPowerOffMachine(typed) {
var btn = document.getElementById('btn-poweroff');
var status = document.getElementById('poweroff-status');
if (btn) btn.disabled = true;
var h = csrfHeaders(); h['Content-Type'] = 'application/json';
fetch('/api/server/poweroff', { method: 'POST', headers: h, body: JSON.stringify({confirm: typed}) })
.then(function(r){ return r.json().catch(function(){ return {ok:false}; }); })
.then(function(d){
if (d && d.ok) {
status.innerHTML = '<div class="alert alert-info">' + escText('{{T "settings_system.a_gep_kikapcsol_folyamatban"}}') + '</div>';
return; // the box goes down — nothing to poll
}
status.innerHTML = '<div class="alert alert-error">' + escText((d && d.error) || '{{T "common.hiba_tortent"}}') + '</div>';
if (btn) btn.disabled = false;
})
.catch(function(){
status.innerHTML = '<div class="alert alert-error">' + escText('{{T "settings_system.kapcsolodasi_hiba"}}') + '</div>';
if (btn) btn.disabled = false;
});
}
function pollRestart(attempt) { function pollRestart(attempt) {
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
document.getElementById('restart-status').innerHTML = document.getElementById('restart-status').innerHTML =
@@ -333,10 +333,15 @@ function openDialog(opts){
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">' 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||'Megerősítés')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>' +'<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>'
+(opts.typeToConfirm?'<div class="confirm-input"><label>Megerősítéshez írd be: <strong class="mono">'+escText(opts.typeToConfirm)+'</strong></label>'
+'<input type="text" id="dialog-type" class="form-control" autocomplete="off" placeholder="'+escText(opts.typeToConfirm)+'"></div>':'')
+'<div class="form-actions"><button id="dialog-go" class="btn '+(opts.typeToConfirm?'btn-danger-outline':'btn-primary')+'"'+(opts.typeToConfirm?' disabled':'')+'>'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>' +'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
+'</div></div>'; +'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); }; var typed=document.getElementById('dialog-type');
if(typed){ typed.oninput=function(){ document.getElementById('dialog-go').disabled=(this.value.trim().toUpperCase()!==opts.typeToConfirm.toUpperCase()); }; }
document.getElementById('dialog-go').onclick=function(){ var v=typed?typed.value:''; closeDialog(); if(opts.onConfirm) opts.onConfirm(v); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); }; document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
} }
@@ -479,12 +484,23 @@ function pollUntilBack() {
<div class="settings-card"> <div class="settings-card">
<h3>Kiszolgáló újraindítása</h3> <h3>A Felhom újraindítása</h3>
<p class="settings-card-desc"> <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áld, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő. Újraindítja a Felhom részt a gépen: a vezérlőpultot és az összes alkalmazást. A gép maga bekapcsolva marad. Kb. 30 másodpercig semmi nem érhető el, utána minden magától visszajön. Ha csak a vezérlőpult akad, a fenti „Vezérlő újraindítása” elég.
</p> </p>
<div id="server-restart-status"></div> <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()">A Felhom újraindítása</button>
</div>
<div class="settings-card" id="poweroff-card">
<h3>A gép kikapcsolása</h3>
<p class="settings-card-desc">A gép teljesen kikapcsol. Az alkalmazásaid és a mentések leállnak.</p>
<p class="settings-card-desc"><strong>Újra bekapcsolni csak otthon lehet: nyomd meg a gép bekapcsológombját.</strong></p>
<div id="poweroff-status"></div>
<button type="button" class="btn btn-danger-outline" id="btn-poweroff" onclick="powerOffMachine()">A gép kikapcsolása</button>
</div> </div>
<script> <script>
@@ -503,8 +519,8 @@ function doRestartController() {
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function restartServer() { function restartServer() {
openDialog({title:'Kiszolgáló újraindítása', confirmLabel:'Újraindítás', openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
message:'Biztosan újraindítod a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.', message:'Biztosan újraindítod a Felhomot? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak. A gép bekapcsolva marad.',
onConfirm:doRestartServer}); onConfirm:doRestartServer});
} }
function doRestartServer() { function doRestartServer() {
@@ -516,6 +532,32 @@ function doRestartServer() {
.then(function(){ pollRestart(0); }) .then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function powerOffMachine() {
var word = 'KIKAPCSOLÁS';
openDialog({title:'A gép kikapcsolása', confirmLabel:'Kikapcsolás',
message:'Biztosan kikapcsolod a gépet? A Felhom leáll, és a gép kikapcsol. Távolról senki nem tudja újra bekapcsolni: csak otthon, a gép bekapcsológombjával.', typeToConfirm: word,
onConfirm:doPowerOffMachine});
}
function doPowerOffMachine(typed) {
var btn = document.getElementById('btn-poweroff');
var status = document.getElementById('poweroff-status');
if (btn) btn.disabled = true;
var h = csrfHeaders(); h['Content-Type'] = 'application/json';
fetch('/api/server/poweroff', { method: 'POST', headers: h, body: JSON.stringify({confirm: typed}) })
.then(function(r){ return r.json().catch(function(){ return {ok:false}; }); })
.then(function(d){
if (d && d.ok) {
status.innerHTML = '<div class="alert alert-info">' + escText('A gép kikapcsol. Néhány perc múlva teljesen leáll, és ez az oldal nem válaszol többé. Újra bekapcsolni: nyomd meg a gép bekapcsológombját.') + '</div>';
return;
}
status.innerHTML = '<div class="alert alert-error">' + escText((d && d.error) || 'Hiba történt') + '</div>';
if (btn) btn.disabled = false;
})
.catch(function(){
status.innerHTML = '<div class="alert alert-error">' + escText('Kapcsolódási hiba') + '</div>';
if (btn) btn.disabled = false;
});
}
function pollRestart(attempt) { function pollRestart(attempt) {
if (attempt > 60) { if (attempt > 60) {
document.getElementById('restart-status').innerHTML = document.getElementById('restart-status').innerHTML =
@@ -395,10 +395,15 @@ function openDialog(opts){
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">' 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||'Megerősítés')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>' +'<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>'
+(opts.typeToConfirm?'<div class="confirm-input"><label>Megerősítéshez írd be: <strong class="mono">'+escText(opts.typeToConfirm)+'</strong></label>'
+'<input type="text" id="dialog-type" class="form-control" autocomplete="off" placeholder="'+escText(opts.typeToConfirm)+'"></div>':'')
+'<div class="form-actions"><button id="dialog-go" class="btn '+(opts.typeToConfirm?'btn-danger-outline':'btn-primary')+'"'+(opts.typeToConfirm?' disabled':'')+'>'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>' +'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
+'</div></div>'; +'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); }; var typed=document.getElementById('dialog-type');
if(typed){ typed.oninput=function(){ document.getElementById('dialog-go').disabled=(this.value.trim().toUpperCase()!==opts.typeToConfirm.toUpperCase()); }; }
document.getElementById('dialog-go').onclick=function(){ var v=typed?typed.value:''; closeDialog(); if(opts.onConfirm) opts.onConfirm(v); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); }; document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
} }
@@ -543,12 +548,23 @@ pollUntilBack();
<div class="settings-card"> <div class="settings-card">
<h3>Kiszolgáló újraindítása</h3> <h3>A Felhom újraindítása</h3>
<p class="settings-card-desc"> <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áld, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő. Újraindítja a Felhom részt a gépen: a vezérlőpultot és az összes alkalmazást. A gép maga bekapcsolva marad. Kb. 30 másodpercig semmi nem érhető el, utána minden magától visszajön. Ha csak a vezérlőpult akad, a fenti „Vezérlő újraindítása” elég.
</p> </p>
<div id="server-restart-status"></div> <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()">A Felhom újraindítása</button>
</div>
<div class="settings-card" id="poweroff-card">
<h3>A gép kikapcsolása</h3>
<p class="settings-card-desc">A gép teljesen kikapcsol. Az alkalmazásaid és a mentések leállnak.</p>
<p class="settings-card-desc"><strong>Újra bekapcsolni csak otthon lehet: nyomd meg a gép bekapcsológombját.</strong></p>
<div id="poweroff-status"></div>
<button type="button" class="btn btn-danger-outline" id="btn-poweroff" onclick="powerOffMachine()">A gép kikapcsolása</button>
</div> </div>
<script> <script>
@@ -567,8 +583,8 @@ function doRestartController() {
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function restartServer() { function restartServer() {
openDialog({title:'Kiszolgáló újraindítása', confirmLabel:'Újraindítás', openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
message:'Biztosan újraindítod a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.', message:'Biztosan újraindítod a Felhomot? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak. A gép bekapcsolva marad.',
onConfirm:doRestartServer}); onConfirm:doRestartServer});
} }
function doRestartServer() { function doRestartServer() {
@@ -580,6 +596,32 @@ function doRestartServer() {
.then(function(){ pollRestart(0); }) .then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function powerOffMachine() {
var word = 'KIKAPCSOLÁS';
openDialog({title:'A gép kikapcsolása', confirmLabel:'Kikapcsolás',
message:'Biztosan kikapcsolod a gépet? A Felhom leáll, és a gép kikapcsol. Távolról senki nem tudja újra bekapcsolni: csak otthon, a gép bekapcsológombjával.', typeToConfirm: word,
onConfirm:doPowerOffMachine});
}
function doPowerOffMachine(typed) {
var btn = document.getElementById('btn-poweroff');
var status = document.getElementById('poweroff-status');
if (btn) btn.disabled = true;
var h = csrfHeaders(); h['Content-Type'] = 'application/json';
fetch('/api/server/poweroff', { method: 'POST', headers: h, body: JSON.stringify({confirm: typed}) })
.then(function(r){ return r.json().catch(function(){ return {ok:false}; }); })
.then(function(d){
if (d && d.ok) {
status.innerHTML = '<div class="alert alert-info">' + escText('A gép kikapcsol. Néhány perc múlva teljesen leáll, és ez az oldal nem válaszol többé. Újra bekapcsolni: nyomd meg a gép bekapcsológombját.') + '</div>';
return;
}
status.innerHTML = '<div class="alert alert-error">' + escText((d && d.error) || 'Hiba történt') + '</div>';
if (btn) btn.disabled = false;
})
.catch(function(){
status.innerHTML = '<div class="alert alert-error">' + escText('Kapcsolódási hiba') + '</div>';
if (btn) btn.disabled = false;
});
}
function pollRestart(attempt) { function pollRestart(attempt) {
if (attempt > 60) { if (attempt > 60) {
document.getElementById('restart-status').innerHTML = document.getElementById('restart-status').innerHTML =
@@ -282,10 +282,15 @@ function openDialog(opts){
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">' 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||'Megerősítés')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>' +'<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>'
+(opts.typeToConfirm?'<div class="confirm-input"><label>Megerősítéshez írd be: <strong class="mono">'+escText(opts.typeToConfirm)+'</strong></label>'
+'<input type="text" id="dialog-type" class="form-control" autocomplete="off" placeholder="'+escText(opts.typeToConfirm)+'"></div>':'')
+'<div class="form-actions"><button id="dialog-go" class="btn '+(opts.typeToConfirm?'btn-danger-outline':'btn-primary')+'"'+(opts.typeToConfirm?' disabled':'')+'>'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>' +'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
+'</div></div>'; +'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); }; var typed=document.getElementById('dialog-type');
if(typed){ typed.oninput=function(){ document.getElementById('dialog-go').disabled=(this.value.trim().toUpperCase()!==opts.typeToConfirm.toUpperCase()); }; }
document.getElementById('dialog-go').onclick=function(){ var v=typed?typed.value:''; closeDialog(); if(opts.onConfirm) opts.onConfirm(v); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); }; document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
} }
@@ -428,12 +433,20 @@ function pollUntilBack() {
<div class="settings-card"> <div class="settings-card">
<h3>Kiszolgáló újraindítása</h3> <h3>A Felhom újraindítása</h3>
<p class="settings-card-desc"> <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áld, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő. Újraindítja a Felhom részt a gépen: a vezérlőpultot és az összes alkalmazást. A gép maga bekapcsolva marad. Kb. 30 másodpercig semmi nem érhető el, utána minden magától visszajön. Ha csak a vezérlőpult akad, a fenti „Vezérlő újraindítása” elég.
</p> </p>
<div id="server-restart-status"></div> <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()">A Felhom újraindítása</button>
</div>
<div class="settings-card" id="poweroff-card">
<h3>A gép kikapcsolása</h3>
<p class="settings-card-desc" data-poweroff-unavailable>A gép kikapcsolásához a gép szoftverének frissítése kell. Ez magától megtörténik, addig ez a gomb nem látszik.</p>
</div> </div>
<script> <script>
@@ -452,8 +465,8 @@ function doRestartController() {
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function restartServer() { function restartServer() {
openDialog({title:'Kiszolgáló újraindítása', confirmLabel:'Újraindítás', openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
message:'Biztosan újraindítod a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.', message:'Biztosan újraindítod a Felhomot? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak. A gép bekapcsolva marad.',
onConfirm:doRestartServer}); onConfirm:doRestartServer});
} }
function doRestartServer() { function doRestartServer() {
@@ -465,6 +478,32 @@ function doRestartServer() {
.then(function(){ pollRestart(0); }) .then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function powerOffMachine() {
var word = 'KIKAPCSOLÁS';
openDialog({title:'A gép kikapcsolása', confirmLabel:'Kikapcsolás',
message:'Biztosan kikapcsolod a gépet? A Felhom leáll, és a gép kikapcsol. Távolról senki nem tudja újra bekapcsolni: csak otthon, a gép bekapcsológombjával.', typeToConfirm: word,
onConfirm:doPowerOffMachine});
}
function doPowerOffMachine(typed) {
var btn = document.getElementById('btn-poweroff');
var status = document.getElementById('poweroff-status');
if (btn) btn.disabled = true;
var h = csrfHeaders(); h['Content-Type'] = 'application/json';
fetch('/api/server/poweroff', { method: 'POST', headers: h, body: JSON.stringify({confirm: typed}) })
.then(function(r){ return r.json().catch(function(){ return {ok:false}; }); })
.then(function(d){
if (d && d.ok) {
status.innerHTML = '<div class="alert alert-info">' + escText('A gép kikapcsol. Néhány perc múlva teljesen leáll, és ez az oldal nem válaszol többé. Újra bekapcsolni: nyomd meg a gép bekapcsológombját.') + '</div>';
return;
}
status.innerHTML = '<div class="alert alert-error">' + escText((d && d.error) || 'Hiba történt') + '</div>';
if (btn) btn.disabled = false;
})
.catch(function(){
status.innerHTML = '<div class="alert alert-error">' + escText('Kapcsolódási hiba') + '</div>';
if (btn) btn.disabled = false;
});
}
function pollRestart(attempt) { function pollRestart(attempt) {
if (attempt > 60) { if (attempt > 60) {
document.getElementById('restart-status').innerHTML = document.getElementById('restart-status').innerHTML =
@@ -323,10 +323,15 @@ function openDialog(opts){
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">' 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||'Megerősítés')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>' +'<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>'
+(opts.typeToConfirm?'<div class="confirm-input"><label>Megerősítéshez írd be: <strong class="mono">'+escText(opts.typeToConfirm)+'</strong></label>'
+'<input type="text" id="dialog-type" class="form-control" autocomplete="off" placeholder="'+escText(opts.typeToConfirm)+'"></div>':'')
+'<div class="form-actions"><button id="dialog-go" class="btn '+(opts.typeToConfirm?'btn-danger-outline':'btn-primary')+'"'+(opts.typeToConfirm?' disabled':'')+'>'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>' +'<button type="button" class="btn btn-outline" id="dialog-cancel">Mégsem</button></div>'
+'</div></div>'; +'</div></div>';
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); }; var typed=document.getElementById('dialog-type');
if(typed){ typed.oninput=function(){ document.getElementById('dialog-go').disabled=(this.value.trim().toUpperCase()!==opts.typeToConfirm.toUpperCase()); }; }
document.getElementById('dialog-go').onclick=function(){ var v=typed?typed.value:''; closeDialog(); if(opts.onConfirm) opts.onConfirm(v); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); }; document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
} }
@@ -469,12 +474,23 @@ function pollUntilBack() {
<div class="settings-card"> <div class="settings-card">
<h3>Kiszolgáló újraindítása</h3> <h3>A Felhom újraindítása</h3>
<p class="settings-card-desc"> <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áld, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő. Újraindítja a Felhom részt a gépen: a vezérlőpultot és az összes alkalmazást. A gép maga bekapcsolva marad. Kb. 30 másodpercig semmi nem érhető el, utána minden magától visszajön. Ha csak a vezérlőpult akad, a fenti „Vezérlő újraindítása” elég.
</p> </p>
<div id="server-restart-status"></div> <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()">A Felhom újraindítása</button>
</div>
<div class="settings-card" id="poweroff-card">
<h3>A gép kikapcsolása</h3>
<p class="settings-card-desc">A gép teljesen kikapcsol. Az alkalmazásaid és a mentések leállnak.</p>
<p class="settings-card-desc"><strong>Újra bekapcsolni csak otthon lehet: nyomd meg a gép bekapcsológombját.</strong></p>
<div id="poweroff-status"></div>
<button type="button" class="btn btn-danger-outline" id="btn-poweroff" onclick="powerOffMachine()">A gép kikapcsolása</button>
</div> </div>
<script> <script>
@@ -493,8 +509,8 @@ function doRestartController() {
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function restartServer() { function restartServer() {
openDialog({title:'Kiszolgáló újraindítása', confirmLabel:'Újraindítás', openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
message:'Biztosan újraindítod a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.', message:'Biztosan újraindítod a Felhomot? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak. A gép bekapcsolva marad.',
onConfirm:doRestartServer}); onConfirm:doRestartServer});
} }
function doRestartServer() { function doRestartServer() {
@@ -506,6 +522,32 @@ function doRestartServer() {
.then(function(){ pollRestart(0); }) .then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); .catch(function(){ pollRestart(0); });
} }
function powerOffMachine() {
var word = 'KIKAPCSOLÁS';
openDialog({title:'A gép kikapcsolása', confirmLabel:'Kikapcsolás',
message:'Biztosan kikapcsolod a gépet? A Felhom leáll, és a gép kikapcsol. Távolról senki nem tudja újra bekapcsolni: csak otthon, a gép bekapcsológombjával.', typeToConfirm: word,
onConfirm:doPowerOffMachine});
}
function doPowerOffMachine(typed) {
var btn = document.getElementById('btn-poweroff');
var status = document.getElementById('poweroff-status');
if (btn) btn.disabled = true;
var h = csrfHeaders(); h['Content-Type'] = 'application/json';
fetch('/api/server/poweroff', { method: 'POST', headers: h, body: JSON.stringify({confirm: typed}) })
.then(function(r){ return r.json().catch(function(){ return {ok:false}; }); })
.then(function(d){
if (d && d.ok) {
status.innerHTML = '<div class="alert alert-info">' + escText('A gép kikapcsol. Néhány perc múlva teljesen leáll, és ez az oldal nem válaszol többé. Újra bekapcsolni: nyomd meg a gép bekapcsológombját.') + '</div>';
return;
}
status.innerHTML = '<div class="alert alert-error">' + escText((d && d.error) || 'Hiba történt') + '</div>';
if (btn) btn.disabled = false;
})
.catch(function(){
status.innerHTML = '<div class="alert alert-error">' + escText('Kapcsolódási hiba') + '</div>';
if (btn) btn.disabled = false;
});
}
function pollRestart(attempt) { function pollRestart(attempt) {
if (attempt > 60) { if (attempt > 60) {
document.getElementById('restart-status').innerHTML = document.getElementById('restart-status').innerHTML =
+11 -1
View File
@@ -226,7 +226,17 @@
"event.fill.critical": "BORN AS A KEY (R-521 F7, v0.309.0) -- the fill warning was a Go literal in fillwatch.Message, Hungarian only; moved to the bundle so an English household is told in English, and the R-516 te-form gate then refused its „Kérjük,” opening, so the Hungarian lost that one word (deliberate, not drift). Pinned by TestMessageHungarianWireText (exact bytes) and TestR521F7_HouseholdLanguageRidesTheEvent.", "event.fill.critical": "BORN AS A KEY (R-521 F7, v0.309.0) -- the fill warning was a Go literal in fillwatch.Message, Hungarian only; moved to the bundle so an English household is told in English, and the R-516 te-form gate then refused its „Kérjük,” opening, so the Hungarian lost that one word (deliberate, not drift). Pinned by TestMessageHungarianWireText (exact bytes) and TestR521F7_HouseholdLanguageRidesTheEvent.",
"event.fill.warning": "BORN AS A KEY (R-521 F7, v0.309.0) -- the fill warning was a Go literal in fillwatch.Message, Hungarian only; moved to the bundle so an English household is told in English, and the R-516 te-form gate then refused its „Kérjük,” opening, so the Hungarian lost that one word (deliberate, not drift). Pinned by TestMessageHungarianWireText (exact bytes) and TestR521F7_HouseholdLanguageRidesTheEvent.", "event.fill.warning": "BORN AS A KEY (R-521 F7, v0.309.0) -- the fill warning was a Go literal in fillwatch.Message, Hungarian only; moved to the bundle so an English household is told in English, and the R-516 te-form gate then refused its „Kérjük,” opening, so the Hungarian lost that one word (deliberate, not drift). Pinned by TestMessageHungarianWireText (exact bytes) and TestR521F7_HouseholdLanguageRidesTheEvent.",
"event.fill.label_apps": "BORN AS A KEY (R-521 F7, v0.309.0) -- the built-in fill-watch area's name in the household's language; the Hungarian literal stays in fillTargets as the log/details label, so this key never replaced it. Pinned by TestR521F7_RootFilesystemSendsTheDiskEventInBothLanguages.", "event.fill.label_apps": "BORN AS A KEY (R-521 F7, v0.309.0) -- the built-in fill-watch area's name in the household's language; the Hungarian literal stays in fillTargets as the log/details label, so this key never replaced it. Pinned by TestR521F7_RootFilesystemSendsTheDiskEventInBothLanguages.",
"event.fill.label_system": "BORN AS A KEY (R-521 F7, v0.309.0) -- the built-in fill-watch area's name in the household's language; the Hungarian literal stays in fillTargets as the log/details label, so this key never replaced it. Pinned by TestR521F7_RootFilesystemSendsTheDiskEventInBothLanguages." "event.fill.label_system": "BORN AS A KEY (R-521 F7, v0.309.0) -- the built-in fill-watch area's name in the household's language; the Hungarian literal stays in fillTargets as the log/details label, so this key never replaced it. Pinned by TestR521F7_RootFilesystemSendsTheDiskEventInBothLanguages.",
"poweroff.confirm_required": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_ConfirmationWordRequired.",
"poweroff.agent_down": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence.",
"poweroff.agent_outdated": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_HiddenForOldAgent.",
"poweroff.refused.too_soon": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence.",
"poweroff.refused.already": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence.",
"poweroff.refused.backup": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence.",
"poweroff.refused.update": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence.",
"poweroff.refused.drive": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence.",
"poweroff.refused.other_guests": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence.",
"poweroff.refused.generic": "BORN AS A KEY (R-998, v0.310.0) -- „A gép kikapcsolása”: the household sentence for the power-off route's answer. Pinned by TestPowerOff_EveryReasonHasASentence."
}, },
"flash.share.already_on": "A megosztás már be van kapcsolva.", "flash.share.already_on": "A megosztás már be van kapcsolva.",
"flash.share.enable_failed": "A megosztás bekapcsolása nem sikerült.", "flash.share.enable_failed": "A megosztás bekapcsolása nem sikerült.",