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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 =
|
||||||
|
|||||||
+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">'
|
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 =
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
Reference in New Issue
Block a user