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
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:
@@ -83,6 +83,11 @@ const FeatureRecoveryFailureClass Feature = "recovery_failure_class"
|
||||
// for two months, and it heals itself when the agent updates.
|
||||
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
|
||||
// refuses — the existing agent-error paths speak honestly when the agent is down).
|
||||
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 {
|
||||
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
|
||||
@@ -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
|
||||
// preserves for boxes that have not updated yet.
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -2031,13 +2031,13 @@
|
||||
"settings_system.atmeretezes": "Resize",
|
||||
"settings_system.atmeretezes_2": "Resizing...",
|
||||
"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_uzemelteto_altal_beallitott_ertekek": "Values set by the operator. Ask the operator to change them.",
|
||||
"settings_system.beallitasok_rendszer": "Settings — System",
|
||||
"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_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.csokkentes": "Reduce",
|
||||
"settings_system.ellenorzes": "Checking...",
|
||||
@@ -2063,7 +2063,7 @@
|
||||
"settings_system.jelenlegi_verzio": "Current version",
|
||||
"settings_system.kapcsolodasi_hiba": "Connection error",
|
||||
"settings_system.kesz": "Done.",
|
||||
"settings_system.kiszolgalo_ujrainditasa": "Restart server",
|
||||
"settings_system.kiszolgalo_ujrainditasa": "Restart Felhom",
|
||||
"settings_system.legujabb_verzio": "Latest version",
|
||||
"settings_system.megengedett_tartomany": "Allowed range",
|
||||
"settings_system.megerosites": "Confirm",
|
||||
@@ -2666,5 +2666,24 @@
|
||||
"family_gate.confirm_reset": "Give a new password? The old one stops at once.",
|
||||
"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.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."
|
||||
}
|
||||
|
||||
@@ -2019,13 +2019,13 @@
|
||||
"settings_system.atmeretezes": "Átméretezés",
|
||||
"settings_system.atmeretezes_2": "Átméretezé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_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.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_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.csokkentes": "Csökkentés",
|
||||
"settings_system.ellenorzes": "Ellenőrzés...",
|
||||
@@ -2051,7 +2051,7 @@
|
||||
"settings_system.jelenlegi_verzio": "Jelenlegi verzió",
|
||||
"settings_system.kapcsolodasi_hiba": "Kapcsolódási hiba",
|
||||
"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.megengedett_tartomany": "Megengedett tartomány",
|
||||
"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.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.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."
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
// 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["MemorySupported"], d["MemoryReachable"] = true, true
|
||||
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
|
||||
}},
|
||||
{"settings_system_failed", "settings_system", func() map[string]interface{} {
|
||||
@@ -223,6 +224,7 @@ func i18nCasesA() []i18nCase {
|
||||
d["RegistryAnonymous"] = false
|
||||
d["LastUpdateState"] = map[string]interface{}{"Status": "failed", "Error": "pull failed"}
|
||||
d["MemorySupported"], d["MemoryReachable"], d["MemoryProcMB"] = true, false, 4096
|
||||
d["PowerOffSupported"] = true
|
||||
return d
|
||||
}},
|
||||
{"settings_system_pending", "settings_system", func() map[string]interface{} {
|
||||
@@ -231,11 +233,13 @@ func i18nCasesA() []i18nCase {
|
||||
d["SelfUpdateEnabled"] = true
|
||||
d["LastUpdateState"] = map[string]interface{}{"Status": "pending"}
|
||||
d["MemorySupported"], d["MemoryReachable"], d["MemoryProcMB"] = true, false, 0
|
||||
d["PowerOffSupported"] = true
|
||||
return d
|
||||
}},
|
||||
{"settings_system_nomemory", "settings_system", func() map[string]interface{} {
|
||||
d := i18nLayoutData("settings", "Rendszer beállítások")
|
||||
d["MemorySupported"] = false
|
||||
d["PowerOffSupported"] = false // R-998: an old agent — the card's one-line note instead of the button
|
||||
return d
|
||||
}},
|
||||
{"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())
|
||||
}
|
||||
}
|
||||
@@ -201,6 +201,10 @@ type Server struct {
|
||||
memFeatures agentapi.SupportCache
|
||||
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)
|
||||
assetsSyncer *assets.Syncer
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestSettingsSplitPagesRender(t *testing.T) {
|
||||
mustNot []string
|
||||
}{
|
||||
{"/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"}},
|
||||
{"/settings/notifications",
|
||||
[]string{"Beállítások — Értesítések"},
|
||||
@@ -110,7 +110,7 @@ func TestSettingsSectionInventory(t *testing.T) {
|
||||
"Alkalmazás-email",
|
||||
"Vészhelyzeti információk", // renamed from the misspelled "Veszhelyzeti informaciok"
|
||||
"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
|
||||
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
|
||||
// to activate storage — this button is purely a full-server restart.)
|
||||
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()
|
||||
if 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">'
|
||||
+'<h3>'+escText(opts.title||'{{T "settings_system.megerosites"}}')+'</h3>'
|
||||
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'{{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>'
|
||||
+'</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(); };
|
||||
}
|
||||
|
||||
@@ -395,6 +400,20 @@ pollUntilBack();
|
||||
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">{{T "settings_system.kiszolgalo_ujrainditasa"}}</button>
|
||||
</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>
|
||||
function restartController() {
|
||||
openDialog({title:'{{T "settings_system.vezerlo_ujrainditasa"}}', confirmLabel:'{{T "common.ujrainditas"}}',
|
||||
@@ -424,6 +443,32 @@ function doRestartServer() {
|
||||
.then(function(){ pollRestart(0); })
|
||||
.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) {
|
||||
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
|
||||
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">'
|
||||
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
|
||||
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
|
||||
|
||||
+(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>'
|
||||
+'</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(); };
|
||||
}
|
||||
|
||||
@@ -479,12 +484,23 @@ function pollUntilBack() {
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Kiszolgáló újraindítása</h3>
|
||||
<h3>A Felhom újraindítása</h3>
|
||||
<p class="settings-card-desc">
|
||||
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor haszná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>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
@@ -503,8 +519,8 @@ function doRestartController() {
|
||||
.catch(function(){ pollRestart(0); });
|
||||
}
|
||||
function restartServer() {
|
||||
openDialog({title:'Kiszolgáló ú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.',
|
||||
openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
|
||||
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});
|
||||
}
|
||||
function doRestartServer() {
|
||||
@@ -516,6 +532,32 @@ function doRestartServer() {
|
||||
.then(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) {
|
||||
if (attempt > 60) {
|
||||
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">'
|
||||
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
|
||||
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
|
||||
|
||||
+(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>'
|
||||
+'</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(); };
|
||||
}
|
||||
|
||||
@@ -543,12 +548,23 @@ pollUntilBack();
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Kiszolgáló újraindítása</h3>
|
||||
<h3>A Felhom újraindítása</h3>
|
||||
<p class="settings-card-desc">
|
||||
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor haszná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>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
@@ -567,8 +583,8 @@ function doRestartController() {
|
||||
.catch(function(){ pollRestart(0); });
|
||||
}
|
||||
function restartServer() {
|
||||
openDialog({title:'Kiszolgáló ú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.',
|
||||
openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
|
||||
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});
|
||||
}
|
||||
function doRestartServer() {
|
||||
@@ -580,6 +596,32 @@ function doRestartServer() {
|
||||
.then(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) {
|
||||
if (attempt > 60) {
|
||||
document.getElementById('restart-status').innerHTML =
|
||||
|
||||
+46
-7
@@ -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">'
|
||||
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
|
||||
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
|
||||
|
||||
+(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>'
|
||||
+'</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(); };
|
||||
}
|
||||
|
||||
@@ -428,12 +433,20 @@ function pollUntilBack() {
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Kiszolgáló újraindítása</h3>
|
||||
<h3>A Felhom újraindítása</h3>
|
||||
<p class="settings-card-desc">
|
||||
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor haszná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>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
@@ -452,8 +465,8 @@ function doRestartController() {
|
||||
.catch(function(){ pollRestart(0); });
|
||||
}
|
||||
function restartServer() {
|
||||
openDialog({title:'Kiszolgáló ú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.',
|
||||
openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
|
||||
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});
|
||||
}
|
||||
function doRestartServer() {
|
||||
@@ -465,6 +478,32 @@ function doRestartServer() {
|
||||
.then(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) {
|
||||
if (attempt > 60) {
|
||||
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">'
|
||||
+'<h3>'+escText(opts.title||'Megerősítés')+'</h3>'
|
||||
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Megerősítés')+'</button>'
|
||||
|
||||
+(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>'
|
||||
+'</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(); };
|
||||
}
|
||||
|
||||
@@ -469,12 +474,23 @@ function pollUntilBack() {
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Kiszolgáló újraindítása</h3>
|
||||
<h3>A Felhom újraindítása</h3>
|
||||
<p class="settings-card-desc">
|
||||
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor haszná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>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
@@ -493,8 +509,8 @@ function doRestartController() {
|
||||
.catch(function(){ pollRestart(0); });
|
||||
}
|
||||
function restartServer() {
|
||||
openDialog({title:'Kiszolgáló ú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.',
|
||||
openDialog({title:'A Felhom újraindítása', confirmLabel:'Újraindítás',
|
||||
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});
|
||||
}
|
||||
function doRestartServer() {
|
||||
@@ -506,6 +522,32 @@ function doRestartServer() {
|
||||
.then(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) {
|
||||
if (attempt > 60) {
|
||||
document.getElementById('restart-status').innerHTML =
|
||||
|
||||
Reference in New Issue
Block a user