v0.143.0 — guest RAM resize UI (R-24) — MinAgent: 0.90.0
The customer sees the guest's current memory + allowed range on the Rendszer settings page and resizes it. The controller proxies + maps the agent's machine code to Hungarian; the agent (felhom-agent v0.90.0) enforces every bound and applies live (no reboot). agentapi: GuestMemory + ResizeMemory; ruled 412 -> *MemoryRefusedError (code+bounds); pre-0.90 agent 404 -> typed *StatusError. Capability: FeatureGuestMemoryResize + featureMinAgent 0.90.0 + a featureProbes row (type-asserts GuestMemory so the shared SupportProber/netAgent are untouched). UI (internal/web/system_memory_handlers.go, settings_system.html): "Szerver memoria (RAM)" card + number input; POST /api/system/memory/resize -> capability gate -> agent -> flash. JS confirm only on shrink. Code->Hungarian map; agent-outdated hides the control; agent-unreachable falls back to the guest's /proc/meminfo. Agent English never shown raw. Tests: agentapi (decode, 404, refusal-code, capability table) + web handler (success/ below_usage_floor/agent_outdated). Gates + go build/vet/test all pass.
This commit is contained in:
@@ -1,5 +1,35 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.143.0 — guest RAM resize UI (R-24) — MinAgent: 0.90.0 (2026-07-17)
|
||||||
|
|
||||||
|
The customer sees the guest's current memory + the allowed range on the **Rendszer** settings page and
|
||||||
|
resizes it. The controller only proxies + maps the agent's machine `code` to Hungarian — the AGENT
|
||||||
|
(felhom-agent v0.90.0) enforces every bound and applies the change live (no reboot). Memory only.
|
||||||
|
|
||||||
|
- **agentapi (`internal/agentapi/client.go`):** `GuestMemory(ctx)` (GET /guest/memory) and
|
||||||
|
`ResizeMemory(ctx, mb)` (POST /guest/memory). A ruled 412 refusal surfaces `*MemoryRefusedError`
|
||||||
|
carrying the machine code (below_min/above_max/below_usage_floor) + fresh bounds; a pre-0.90 agent
|
||||||
|
404s → the typed `*StatusError{404}` (the capability signal).
|
||||||
|
- **Capability (`internal/agentapi/features.go`):** `FeatureGuestMemoryResize` + `featureMinAgent`
|
||||||
|
**0.90.0** + a `featureProbes` row (GET /guest/memory is the probe; the probe type-asserts the one
|
||||||
|
method it needs, so the shared `SupportProber`/`netAgent` stay untouched). Per the publish-train
|
||||||
|
convention (this CHANGELOG declares MinAgent; the Supports gate sits at the handler entry point).
|
||||||
|
- **UI (`internal/web/system_memory_handlers.go`, new; `templates/settings_system.html`):** a "Szerver
|
||||||
|
memória (RAM)" card shows current/used memory + the `2048 MB – {max} MB` range; a number input
|
||||||
|
(step 256) + "Átméretezés". `POST /api/system/memory/resize` → capability gate (SupportUnknown passes)
|
||||||
|
→ agent → POST-response flash. A JS confirm fires ONLY on a shrink. Code→Hungarian map: success
|
||||||
|
"A memória átméretezése megtörtént: X MB → Y MB."; below_usage_floor "…túl közel van a jelenlegi
|
||||||
|
felhasználáshoz (N MB). Állíts le néhány alkalmazást…"; below_min / above_max; agent-outdated → the
|
||||||
|
control is not offered + a "rendszerfrissítése szükséges" note; agent-unreachable → the value falls
|
||||||
|
back to the guest's own `/proc/meminfo`, control disabled, honest note (the page never 500s). The
|
||||||
|
agent's English message is never shown raw.
|
||||||
|
- **Ripple (no code):** lxcfs updates the guest `/proc/meminfo` live, so the deploy-page memory math
|
||||||
|
follows a resize automatically.
|
||||||
|
- **Tests:** agentapi (GuestMemory decode, 404-typed, ResizeMemory success + refusal-code, capability
|
||||||
|
table 0.89→No / 0.90→Yes / probe 404→No / probe ok→Yes) + web handler (success maps to Hungarian;
|
||||||
|
below_usage_floor maps + the agent English never leaks; agent_outdated gate refuses with ResizeMemory
|
||||||
|
never called). Gates: template_id + emoji OK; `go build/vet/test` all pass.
|
||||||
|
|
||||||
### v0.142.0 — offsite repo continuity: orphaned-repo guard (A) + run-status auto-refresh (C) (2026-07-17)
|
### v0.142.0 — offsite repo continuity: orphaned-repo guard (A) + run-status auto-refresh (C) (2026-07-17)
|
||||||
|
|
||||||
Closes the reinstall-orphaned-repo incident class (`memory` offbox-repo-orphaned-2026-07-17): a
|
Closes the reinstall-orphaned-repo incident class (`memory` offbox-repo-orphaned-2026-07-17): a
|
||||||
|
|||||||
@@ -1009,6 +1009,8 @@ func main() {
|
|||||||
mux.Handle("/api/disks/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeDiskAPI))))
|
mux.Handle("/api/disks/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeDiskAPI))))
|
||||||
// Guided storage provisioning (init/attach/eject orchestration over the agent disk API + registry).
|
// Guided storage provisioning (init/attach/eject orchestration over the agent disk API + registry).
|
||||||
mux.Handle("/api/storage/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeStorageAPI))))
|
mux.Handle("/api/storage/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeStorageAPI))))
|
||||||
|
// Guest RAM resize (v0.143.0, R-24): read current allocation/bounds + apply a bounded resize.
|
||||||
|
mux.Handle("/api/system/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeSystemAPI))))
|
||||||
// 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))))
|
||||||
|
|||||||
@@ -454,6 +454,101 @@ func (c *Client) GuestReboot(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- v0.143.0: guest RAM resize (R-24, agent ≥ 0.90.0) ------------------------------------
|
||||||
|
|
||||||
|
// GuestMemoryInfo mirrors the agent's GET /guest/memory (every field MB, agent-computed). The
|
||||||
|
// min/max/floor are the CURRENTLY-enforced bounds — the UI renders them but the agent re-checks fresh.
|
||||||
|
type GuestMemoryInfo struct {
|
||||||
|
VMID int `json:"vmid"`
|
||||||
|
AllocatedMB int64 `json:"allocated_mb"`
|
||||||
|
UsageMB int64 `json:"usage_mb"`
|
||||||
|
HostTotalMB int64 `json:"host_total_mb"`
|
||||||
|
MinMB int64 `json:"min_mb"`
|
||||||
|
MaxMB int64 `json:"max_mb"`
|
||||||
|
FloorMB int64 `json:"floor_mb"`
|
||||||
|
Running bool `json:"running"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryResizeResult mirrors the agent's POST /guest/memory success body.
|
||||||
|
type MemoryResizeResult struct {
|
||||||
|
VMID int `json:"vmid"`
|
||||||
|
OldMB int64 `json:"old_mb"`
|
||||||
|
NewMB int64 `json:"new_mb"`
|
||||||
|
Unchanged bool `json:"unchanged"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryRefusedError carries the agent's machine refusal code (below_min | above_max |
|
||||||
|
// below_usage_floor) plus the fresh bounds, so the web layer maps it to a Hungarian message and
|
||||||
|
// re-renders the range honestly — the agent's English message is never shown raw.
|
||||||
|
type MemoryRefusedError struct {
|
||||||
|
Code string
|
||||||
|
Bounds GuestMemoryInfo
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MemoryRefusedError) Error() string {
|
||||||
|
return "agentapi: memory resize refused (" + e.Code + "): " + e.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// GuestMemory reads the guest's current allocation, live usage, and the enforced bounds. A pre-0.90
|
||||||
|
// agent has no such route → the get helper returns *StatusError{404} (the capability probe signal
|
||||||
|
// and the UI's "needs an update" path).
|
||||||
|
func (c *Client) GuestMemory(ctx context.Context) (GuestMemoryInfo, error) {
|
||||||
|
var out GuestMemoryInfo
|
||||||
|
data, err := c.get(ctx, "/guest/memory")
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &out); err != nil {
|
||||||
|
return out, fmt.Errorf("agentapi: decode /guest/memory: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeMemory requests a bounded resize. The agent enforces every bound; a ruled refusal (412)
|
||||||
|
// returns *MemoryRefusedError carrying the code + fresh bounds; a non-coded failure (e.g. the 502
|
||||||
|
// verify-after-apply) returns a plain error; success returns old→new.
|
||||||
|
func (c *Client) ResizeMemory(ctx context.Context, memoryMB int64) (MemoryResizeResult, error) {
|
||||||
|
var out MemoryResizeResult
|
||||||
|
env, status, err := c.postWithStatus(ctx, "/guest/memory", map[string]int64{"memory_mb": memoryMB})
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if status == http.StatusOK && env.OK {
|
||||||
|
if len(env.Data) > 0 {
|
||||||
|
_ = json.Unmarshal(env.Data, &out)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
// Refusal — the data carries {code, ...fresh bounds}.
|
||||||
|
var ref struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
AllocatedMB int64 `json:"allocated_mb"`
|
||||||
|
UsageMB int64 `json:"usage_mb"`
|
||||||
|
HostTotalMB int64 `json:"host_total_mb"`
|
||||||
|
MinMB int64 `json:"min_mb"`
|
||||||
|
MaxMB int64 `json:"max_mb"`
|
||||||
|
FloorMB int64 `json:"floor_mb"`
|
||||||
|
}
|
||||||
|
if len(env.Data) > 0 {
|
||||||
|
_ = json.Unmarshal(env.Data, &ref)
|
||||||
|
}
|
||||||
|
if ref.Code != "" {
|
||||||
|
return out, &MemoryRefusedError{
|
||||||
|
Code: ref.Code,
|
||||||
|
Bounds: GuestMemoryInfo{
|
||||||
|
AllocatedMB: ref.AllocatedMB, UsageMB: ref.UsageMB, HostTotalMB: ref.HostTotalMB,
|
||||||
|
MinMB: ref.MinMB, MaxMB: ref.MaxMB, FloorMB: ref.FloorMB,
|
||||||
|
},
|
||||||
|
Msg: truncateErr(env.Error, 300),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rerr := refusalError("/guest/memory", status, env); rerr != nil {
|
||||||
|
return out, rerr
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SwapResult mirrors the agent's 202 from POST /controller/swap (agentic controller update, Phase 1).
|
// SwapResult mirrors the agent's 202 from POST /controller/swap (agentic controller update, Phase 1).
|
||||||
type SwapResult struct {
|
type SwapResult struct {
|
||||||
Status string `json:"status"` // "swapping"
|
Status string `json:"status"` // "swapping"
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ type Feature string
|
|||||||
// capability signal.
|
// capability signal.
|
||||||
const FeatureNetstorageVerify Feature = "netstorage_verify"
|
const FeatureNetstorageVerify Feature = "netstorage_verify"
|
||||||
|
|
||||||
|
// FeatureGuestMemoryResize is the guest RAM resize (agent v0.90.0, R-24): the resize endpoints
|
||||||
|
// (GET/POST /guest/memory) shipped together, so GET /guest/memory IS the capability signal.
|
||||||
|
const FeatureGuestMemoryResize Feature = "guest_memory_resize"
|
||||||
|
|
||||||
// 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
|
||||||
@@ -67,14 +71,34 @@ var featureProbes = map[Feature]func(ctx context.Context, p SupportProber) error
|
|||||||
_, err := p.NetVerifyStatus(ctx)
|
_, err := p.NetVerifyStatus(ctx)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
|
// The memory-resize prober needs GET /guest/memory, not NetVerifyStatus. Rather than couple the
|
||||||
|
// shared SupportProber (and every unrelated prober/fake) to the memory surface, the probe
|
||||||
|
// type-asserts the ONE method it needs — the memory feature is only ever probed with a
|
||||||
|
// GuestMemory-capable prober (the web memAgent seam / *Client). A prober without it → a non-404
|
||||||
|
// error → SupportUnknown (fail-open), never a false "supported".
|
||||||
|
FeatureGuestMemoryResize: func(ctx context.Context, p SupportProber) error {
|
||||||
|
gm, ok := p.(interface {
|
||||||
|
GuestMemory(ctx context.Context) (GuestMemoryInfo, error)
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
return errNoMemoryProbe
|
||||||
|
}
|
||||||
|
_, err := gm.GuestMemory(ctx)
|
||||||
|
return err
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errNoMemoryProbe classifies to SupportUnknown (not a *StatusError 404), so a prober that cannot be
|
||||||
|
// asked never reads as "unsupported".
|
||||||
|
var errNoMemoryProbe = errors.New("agentapi: prober does not support the guest-memory probe")
|
||||||
|
|
||||||
// featureMinAgent maps each coupled feature to the MINIMUM agent version that carries its coupled
|
// featureMinAgent maps each coupled feature to the MINIMUM agent version that carries its coupled
|
||||||
// semantics (the CHANGELOG `MinAgent:` header value). Used by Supports when the agent's version is
|
// semantics (the CHANGELOG `MinAgent:` header value). Used by Supports when the agent's version is
|
||||||
// KNOWN (the v0.82.0 X-Felhom-Agent-Version channel) — a direct comparison, no probe traffic. A
|
// KNOWN (the v0.82.0 X-Felhom-Agent-Version channel) — a direct comparison, no probe traffic. A
|
||||||
// feature missing here (or an unparseable table value) falls back to the probe.
|
// feature missing here (or an unparseable table value) falls back to the probe.
|
||||||
var featureMinAgent = map[Feature]string{
|
var featureMinAgent = map[Feature]string{
|
||||||
FeatureNetstorageVerify: "0.81.0",
|
FeatureNetstorageVerify: "0.81.0",
|
||||||
|
FeatureGuestMemoryResize: "0.90.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentVersionReporter is optionally implemented by a SupportProber (*Client is one): it reports
|
// AgentVersionReporter is optionally implemented by a SupportProber (*Client is one): it reports
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package agentapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// memProber satisfies the SupportProber contract (NetVerifyStatus) PLUS the memory probe's
|
||||||
|
// type-asserted GuestMemory + the AgentVersion fast-path — the shape the web memAgent seam has.
|
||||||
|
type memProber struct {
|
||||||
|
memErr error
|
||||||
|
ver string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *memProber) NetVerifyStatus(context.Context) (NetVerifyStatus, error) {
|
||||||
|
return NetVerifyStatus{Phase: "none"}, nil
|
||||||
|
}
|
||||||
|
func (p *memProber) GuestMemory(context.Context) (GuestMemoryInfo, error) {
|
||||||
|
return GuestMemoryInfo{AllocatedMB: 8192}, p.memErr
|
||||||
|
}
|
||||||
|
func (p *memProber) AgentVersion() string { return p.ver }
|
||||||
|
|
||||||
|
// The capability table: v0.90.0 → Yes, v0.89.0 → No via the version fast-path; the probe (no
|
||||||
|
// version) classifies 404 → No, nil → Yes.
|
||||||
|
func TestGuestMemory_Capability(t *testing.T) {
|
||||||
|
t.Run("version >= 0.90 → Yes", func(t *testing.T) {
|
||||||
|
var sc SupportCache
|
||||||
|
if got := sc.Supports(context.Background(), &memProber{ver: "0.90.0"}, FeatureGuestMemoryResize); got != SupportYes {
|
||||||
|
t.Errorf("v0.90.0 = %v, want SupportYes", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("version < 0.90 → No", func(t *testing.T) {
|
||||||
|
var sc SupportCache
|
||||||
|
if got := sc.Supports(context.Background(), &memProber{ver: "0.89.0"}, FeatureGuestMemoryResize); got != SupportNo {
|
||||||
|
t.Errorf("v0.89.0 = %v, want SupportNo", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("no version, probe 404 → No", func(t *testing.T) {
|
||||||
|
var sc SupportCache
|
||||||
|
p := &memProber{memErr: &StatusError{Path: "/guest/memory", Code: http.StatusNotFound}}
|
||||||
|
if got := sc.Supports(context.Background(), p, FeatureGuestMemoryResize); got != SupportNo {
|
||||||
|
t.Errorf("probe 404 = %v, want SupportNo", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("no version, probe ok → Yes", func(t *testing.T) {
|
||||||
|
var sc SupportCache
|
||||||
|
if got := sc.Supports(context.Background(), &memProber{}, FeatureGuestMemoryResize); got != SupportYes {
|
||||||
|
t.Errorf("probe ok = %v, want SupportYes", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GuestMemory decodes the agent's payload; a 404 (pre-0.90) is the typed *StatusError.
|
||||||
|
func TestClient_GuestMemory(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"allocated_mb":8192,"usage_mb":3000,"host_total_mb":16384,"min_mb":2048,"max_mb":14336,"floor_mb":3512,"running":true}}`))
|
||||||
|
})
|
||||||
|
c := newMemTestClient(t, mux)
|
||||||
|
info, err := c.GuestMemory(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GuestMemory: %v", err)
|
||||||
|
}
|
||||||
|
if info.AllocatedMB != 8192 || info.UsageMB != 3000 || info.MaxMB != 14336 || info.FloorMB != 3512 || !info.Running {
|
||||||
|
t.Errorf("decoded wrong: %+v", info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GuestMemory_404(t *testing.T) {
|
||||||
|
c := newMemTestClient(t, http.NewServeMux()) // no route → 404
|
||||||
|
_, err := c.GuestMemory(context.Background())
|
||||||
|
var se *StatusError
|
||||||
|
if !errors.As(err, &se) || se.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("pre-0.90 agent must 404 as *StatusError, got %T: %v", err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeMemory: success returns old→new; a 412 refusal surfaces *MemoryRefusedError with the code.
|
||||||
|
func TestClient_ResizeMemory(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":9201,"old_mb":8192,"new_mb":12288,"unchanged":false}}`))
|
||||||
|
})
|
||||||
|
c := newMemTestClient(t, mux)
|
||||||
|
res, err := c.ResizeMemory(context.Background(), 12288)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResizeMemory: %v", err)
|
||||||
|
}
|
||||||
|
if res.OldMB != 8192 || res.NewMB != 12288 {
|
||||||
|
t.Errorf("result = %+v", res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("refusal carries the code", func(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/guest/memory", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusPreconditionFailed)
|
||||||
|
_, _ = w.Write([]byte(`{"ok":false,"error":"requested 3300 MB is too close to current usage 3000 MB (floor 3512 MB)","data":{"code":"below_usage_floor","usage_mb":3000,"floor_mb":3512,"min_mb":2048,"max_mb":14336}}`))
|
||||||
|
})
|
||||||
|
c := newMemTestClient(t, mux)
|
||||||
|
_, err := c.ResizeMemory(context.Background(), 3300)
|
||||||
|
var refused *MemoryRefusedError
|
||||||
|
if !errors.As(err, &refused) {
|
||||||
|
t.Fatalf("want *MemoryRefusedError, got %T: %v", err, err)
|
||||||
|
}
|
||||||
|
if refused.Code != "below_usage_floor" || refused.Bounds.UsageMB != 3000 || refused.Bounds.FloorMB != 3512 {
|
||||||
|
t.Errorf("refusal = %+v", refused)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemTestClient(t *testing.T, mux *http.ServeMux) *Client {
|
||||||
|
t.Helper()
|
||||||
|
srv := httptest.NewTLSServer(mux)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
fp := sha256.Sum256(srv.Certificate().Raw)
|
||||||
|
c, err := New(strings.TrimPrefix(srv.URL, "https://"), "test-token", hex.EncodeToString(fp[:]))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New: %v", err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
@@ -1198,6 +1198,8 @@ func (s *Server) systemPageData() map[string]interface{} {
|
|||||||
// to. Empty = none set by the operator.
|
// to. Empty = none set by the operator.
|
||||||
data["ControllerFloor"] = s.updater.GetFloor()
|
data["ControllerFloor"] = s.updater.GetFloor()
|
||||||
}
|
}
|
||||||
|
// Guest RAM resize card (v0.143.0, R-24): current allocation + bounds + capability/reachability.
|
||||||
|
s.memoryCardData(data)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,11 @@ type Server struct {
|
|||||||
// semantics — the add gate + the settings-page banner read it. Zero value ready.
|
// semantics — the add gate + the settings-page banner read it. Zero value ready.
|
||||||
netFeatures agentapi.SupportCache
|
netFeatures agentapi.SupportCache
|
||||||
|
|
||||||
|
// memFeatures caches the guest-memory-resize capability probe (agent v0.90.0, R-24) — the resize
|
||||||
|
// handler gate + the settings-page render read it. Zero value ready. memAgentFn is the test seam.
|
||||||
|
memFeatures agentapi.SupportCache
|
||||||
|
memAgentFn func() (memAgent, error)
|
||||||
|
|
||||||
// Asset syncer for Hub-managed assets (optional)
|
// Asset syncer for Hub-managed assets (optional)
|
||||||
assetsSyncer *assets.Syncer
|
assetsSyncer *assets.Syncer
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Guest RAM resize UI (v0.143.0, R-24; coupled to agent ≥ 0.90.0). The customer sees the guest's
|
||||||
|
// current memory + the allowed range on the Rendszer settings page and resizes it; the request goes
|
||||||
|
// to the agent's self-scoped /guest/memory, which enforces every bound. The controller only maps the
|
||||||
|
// agent's machine `code` to a Hungarian message — the agent's English text is never shown raw.
|
||||||
|
|
||||||
|
// memAgent is the agent surface the memory UI needs (seam — tests inject a fake; production is the
|
||||||
|
// shared *agentapi.Client). It carries NetVerifyStatus + AgentVersion so it satisfies the shared
|
||||||
|
// SupportProber / version fast-path used by the capability cache (the agent has them anyway).
|
||||||
|
type memAgent interface {
|
||||||
|
GuestMemory(ctx context.Context) (agentapi.GuestMemoryInfo, error)
|
||||||
|
ResizeMemory(ctx context.Context, memoryMB int64) (agentapi.MemoryResizeResult, error)
|
||||||
|
NetVerifyStatus(ctx context.Context) (agentapi.NetVerifyStatus, error) // to satisfy SupportProber
|
||||||
|
AgentVersion() string // version fast-path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) memAgentForResize() (memAgent, error) {
|
||||||
|
if s.memAgentFn != nil {
|
||||||
|
return s.memAgentFn()
|
||||||
|
}
|
||||||
|
c, err := s.agentClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// memUpdateNeededMsg is shown when the agent predates the resize endpoints (pre-0.90).
|
||||||
|
const memUpdateNeededMsg = "A memória átméretezéséhez a kiszolgáló rendszerfrissítése szükséges."
|
||||||
|
|
||||||
|
// memAgentDownMsg is shown when the agent is unreachable — the value falls back to /proc/meminfo.
|
||||||
|
const memAgentDownMsg = "A kiszolgáló ügynök jelenleg nem elérhető — próbáld meg később."
|
||||||
|
|
||||||
|
// memoryResizeMessage maps the agent's machine code (or success) to the customer's Hungarian line.
|
||||||
|
func memoryResizeMessage(code string, res agentapi.MemoryResizeResult, bounds agentapi.GuestMemoryInfo) string {
|
||||||
|
switch code {
|
||||||
|
case "": // success
|
||||||
|
if res.Unchanged {
|
||||||
|
return fmt.Sprintf("A memória változatlan maradt: %d MB.", res.NewMB)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("A memória átméretezése megtörtént: %d MB → %d MB.", res.OldMB, res.NewMB)
|
||||||
|
case "below_usage_floor":
|
||||||
|
return fmt.Sprintf("A kért méret túl közel van a jelenlegi felhasználáshoz (%d MB). Állíts le néhány alkalmazást, majd próbáld újra.", bounds.UsageMB)
|
||||||
|
case "below_min":
|
||||||
|
return fmt.Sprintf("A minimális memória %d MB.", bounds.MinMB)
|
||||||
|
case "above_max":
|
||||||
|
return fmt.Sprintf("A megengedett maximum %d MB (a gazdagép tartaléka miatt).", bounds.MaxMB)
|
||||||
|
default:
|
||||||
|
return "A memória átméretezése nem sikerült. Próbáld meg később."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// memoryCardData fills the settings-page memory card. It never returns an error — every failure mode
|
||||||
|
// degrades to an honest, render-safe state (supported=false, reachable=false + /proc/meminfo value).
|
||||||
|
func (s *Server) memoryCardData(data map[string]interface{}) {
|
||||||
|
data["MemorySupported"] = true // default; flipped to false only on a definite SupportNo
|
||||||
|
data["MemoryReachable"] = false
|
||||||
|
data["MemoryProcMB"] = procMemTotalMB() // in-guest fallback (this container's own /proc/meminfo)
|
||||||
|
|
||||||
|
agent, err := s.memAgentForResize()
|
||||||
|
if err != nil {
|
||||||
|
return // agent not configured/reachable → reachable=false, proc fallback rendered
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Capability gate: only a DEFINITE SupportNo hides the control (SupportUnknown from a down agent
|
||||||
|
// keeps it, with the honest unreachable note).
|
||||||
|
if s.memFeatures.Supports(ctx, agent, agentapi.FeatureGuestMemoryResize) == agentapi.SupportNo {
|
||||||
|
data["MemorySupported"] = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, err := agent.GuestMemory(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logx.Debugf(s.logger, "[web] memory card: GuestMemory failed: %v", err)
|
||||||
|
return // reachable=false; the proc fallback shows the current size
|
||||||
|
}
|
||||||
|
data["MemoryReachable"] = true
|
||||||
|
data["MemoryAllocatedMB"] = info.AllocatedMB
|
||||||
|
data["MemoryUsageMB"] = info.UsageMB
|
||||||
|
data["MemoryMinMB"] = info.MinMB
|
||||||
|
data["MemoryMaxMB"] = info.MaxMB
|
||||||
|
data["MemoryFloorMB"] = info.FloorMB
|
||||||
|
data["MemoryHostTotalMB"] = info.HostTotalMB
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeSystemAPI dispatches /api/system/* (currently the guest-memory resize surface).
|
||||||
|
func (s *Server) ServeSystemAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/api/system/memory" && r.Method == http.MethodGet:
|
||||||
|
s.handleMemoryGet(w, r)
|
||||||
|
case r.URL.Path == "/api/system/memory/resize" && r.Method == http.MethodPost:
|
||||||
|
s.handleMemoryResize(w, r)
|
||||||
|
default:
|
||||||
|
writeDiskJSON(w, http.StatusNotFound, false, "ismeretlen végpont", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMemoryGet returns the current allocation + bounds (the JS refreshes the card after a resize).
|
||||||
|
func (s *Server) handleMemoryGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agent, err := s.memAgentForResize()
|
||||||
|
if err != nil {
|
||||||
|
writeDiskJSON(w, http.StatusBadGateway, false, memAgentDownMsg, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.memFeatures.Supports(r.Context(), agent, agentapi.FeatureGuestMemoryResize) == agentapi.SupportNo {
|
||||||
|
writeDiskJSON(w, http.StatusPreconditionFailed, false, memUpdateNeededMsg, map[string]any{"code": "agent_outdated"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, err := agent.GuestMemory(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeDiskJSON(w, http.StatusBadGateway, false, memAgentDownMsg, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeDiskJSON(w, http.StatusOK, true, "", info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMemoryResize validates nothing itself (the agent is the authority) beyond parsing, gates on
|
||||||
|
// capability, calls the agent, and maps the outcome to a Hungarian message + machine code.
|
||||||
|
func (s *Server) handleMemoryResize(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
MemoryMB int64 `json:"memory_mb"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.MemoryMB <= 0 {
|
||||||
|
writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen memória érték", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agent, err := s.memAgentForResize()
|
||||||
|
if err != nil {
|
||||||
|
writeDiskJSON(w, http.StatusBadGateway, false, memAgentDownMsg, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Capability gate (publish-train backstop): refuse honestly on an agent that predates the resize.
|
||||||
|
support, source := s.memFeatures.SupportsWithSource(r.Context(), agent, agentapi.FeatureGuestMemoryResize)
|
||||||
|
logx.Debugf(s.logger, "[web] memory resize gate: %s=%s (source=%s)", agentapi.FeatureGuestMemoryResize, support, source)
|
||||||
|
if support == agentapi.SupportNo {
|
||||||
|
writeDiskJSON(w, http.StatusPreconditionFailed, false, memUpdateNeededMsg, map[string]any{"code": "agent_outdated"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := agent.ResizeMemory(r.Context(), req.MemoryMB)
|
||||||
|
if err != nil {
|
||||||
|
var refused *agentapi.MemoryRefusedError
|
||||||
|
if errors.As(err, &refused) {
|
||||||
|
s.logger.Printf("[INFO] [web] memory resize refused: code=%s target=%d", refused.Code, req.MemoryMB)
|
||||||
|
writeDiskJSON(w, http.StatusPreconditionFailed, false,
|
||||||
|
memoryResizeMessage(refused.Code, agentapi.MemoryResizeResult{}, refused.Bounds),
|
||||||
|
map[string]any{"code": refused.Code})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logx.Debugf(s.logger, "[web] memory resize agent error: %v", err)
|
||||||
|
writeDiskJSON(w, http.StatusBadGateway, false, "A memória átméretezése nem sikerült — próbáld meg később.", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Printf("[INFO] [web] memory resized: %d MB -> %d MB (unchanged=%v)", res.OldMB, res.NewMB, res.Unchanged)
|
||||||
|
writeDiskJSON(w, http.StatusOK, true, memoryResizeMessage("", res, agentapi.GuestMemoryInfo{}),
|
||||||
|
map[string]any{"old_mb": res.OldMB, "new_mb": res.NewMB, "unchanged": res.Unchanged})
|
||||||
|
}
|
||||||
|
|
||||||
|
// procMemTotalMB reads MemTotal from this container's /proc/meminfo (the agent-unreachable fallback).
|
||||||
|
// Returns 0 if unreadable (the template renders "n/a" then).
|
||||||
|
func procMemTotalMB() int64 {
|
||||||
|
raw, err := os.ReadFile("/proc/meminfo")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(raw), "\n") {
|
||||||
|
if strings.HasPrefix(line, "MemTotal:") {
|
||||||
|
f := strings.Fields(line)
|
||||||
|
if len(f) >= 2 {
|
||||||
|
if kb, err := strconv.ParseInt(f[1], 10, 64); err == nil {
|
||||||
|
return kb / 1024 // kB → MB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeMemAgent is the memAgent seam for handler tests. AgentVersion drives the capability gate
|
||||||
|
// (version fast-path); NetVerifyStatus is the SupportProber stub.
|
||||||
|
type fakeMemAgent struct {
|
||||||
|
info agentapi.GuestMemoryInfo
|
||||||
|
resizeRes agentapi.MemoryResizeResult
|
||||||
|
resizeErr error
|
||||||
|
ver string
|
||||||
|
calls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeMemAgent) GuestMemory(context.Context) (agentapi.GuestMemoryInfo, error) {
|
||||||
|
return f.info, nil
|
||||||
|
}
|
||||||
|
func (f *fakeMemAgent) ResizeMemory(_ context.Context, _ int64) (agentapi.MemoryResizeResult, error) {
|
||||||
|
f.calls++
|
||||||
|
return f.resizeRes, f.resizeErr
|
||||||
|
}
|
||||||
|
func (f *fakeMemAgent) NetVerifyStatus(context.Context) (agentapi.NetVerifyStatus, error) {
|
||||||
|
return agentapi.NetVerifyStatus{Phase: "none"}, nil
|
||||||
|
}
|
||||||
|
func (f *fakeMemAgent) AgentVersion() string { return f.ver }
|
||||||
|
|
||||||
|
func postResize(t *testing.T, s *Server, body string) *httptest.ResponseRecorder {
|
||||||
|
t.Helper()
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/api/system/memory/resize", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.ServeSystemAPI(w, r)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success → 200 with the Hungarian old→new message.
|
||||||
|
func TestMemoryResize_Success(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
agent := &fakeMemAgent{ver: "0.90.0", resizeRes: agentapi.MemoryResizeResult{OldMB: 8192, NewMB: 12288}}
|
||||||
|
s.memAgentFn = func() (memAgent, error) { return agent, nil }
|
||||||
|
|
||||||
|
w := postResize(t, s, `{"memory_mb":12288}`)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("resize = %d (%s), want 200", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), "A memória átméretezése megtörtént: 8192 MB → 12288 MB.") {
|
||||||
|
t.Errorf("success message missing: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
if agent.calls != 1 {
|
||||||
|
t.Errorf("ResizeMemory calls = %d, want 1", agent.calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// below_usage_floor refusal → 412, the mapped Hungarian message, and the machine code (agent English
|
||||||
|
// never shown raw).
|
||||||
|
func TestMemoryResize_BelowUsageFloor(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
agent := &fakeMemAgent{
|
||||||
|
ver: "0.90.0",
|
||||||
|
resizeErr: &agentapi.MemoryRefusedError{
|
||||||
|
Code: "below_usage_floor",
|
||||||
|
Bounds: agentapi.GuestMemoryInfo{UsageMB: 3000, FloorMB: 3512, MinMB: 2048, MaxMB: 14336},
|
||||||
|
Msg: "requested 3300 MB is too close to current usage 3000 MB (floor 3512 MB)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s.memAgentFn = func() (memAgent, error) { return agent, nil }
|
||||||
|
|
||||||
|
w := postResize(t, s, `{"memory_mb":3300}`)
|
||||||
|
if w.Code != http.StatusPreconditionFailed {
|
||||||
|
t.Fatalf("below_usage_floor = %d (%s), want 412", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, `"code":"below_usage_floor"`) {
|
||||||
|
t.Errorf("machine code missing: %s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "túl közel van a jelenlegi felhasználáshoz (3000 MB)") {
|
||||||
|
t.Errorf("mapped Hungarian message missing: %s", body)
|
||||||
|
}
|
||||||
|
// The agent's raw English must NEVER surface.
|
||||||
|
if strings.Contains(body, "requested 3300 MB is too close") {
|
||||||
|
t.Errorf("agent English leaked to the customer: %s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pre-0.90 agent (version < MinAgent) → the capability gate refuses up front with agent_outdated,
|
||||||
|
// and ResizeMemory is NEVER called.
|
||||||
|
func TestMemoryResize_AgentOutdated(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
agent := &fakeMemAgent{ver: "0.89.0"}
|
||||||
|
s.memAgentFn = func() (memAgent, error) { return agent, nil }
|
||||||
|
|
||||||
|
w := postResize(t, s, `{"memory_mb":12288}`)
|
||||||
|
if w.Code != http.StatusPreconditionFailed {
|
||||||
|
t.Fatalf("outdated agent = %d (%s), want 412", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), `"code":"agent_outdated"`) {
|
||||||
|
t.Errorf("agent_outdated code missing: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), "rendszerfrissítése szükséges") {
|
||||||
|
t.Errorf("outdated Hungarian note missing: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
if agent.calls != 0 {
|
||||||
|
t.Errorf("ResizeMemory called %d times on a gated request — must be 0", agent.calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,6 +143,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Szerver memória (RAM)</h3>
|
||||||
|
{{if .MemorySupported}}
|
||||||
|
{{if .MemoryReachable}}
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Jelenlegi memória</span>
|
||||||
|
<span class="settings-value mono">{{.MemoryAllocatedMB}} MB</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Felhasznált</span>
|
||||||
|
<span class="settings-value mono">{{.MemoryUsageMB}} MB</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Megengedett tartomány</span>
|
||||||
|
<span class="settings-value mono">{{.MemoryMinMB}} MB – {{.MemoryMaxMB}} MB</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row" style="padding-top: 0.5em;">
|
||||||
|
<span class="settings-label">Új méret (MB)</span>
|
||||||
|
<span class="settings-value">
|
||||||
|
<input type="number" id="mem-input" class="input mono" style="width:8em;"
|
||||||
|
value="{{.MemoryAllocatedMB}}" min="{{.MemoryMinMB}}" max="{{.MemoryMaxMB}}" step="256"
|
||||||
|
data-current="{{.MemoryAllocatedMB}}">
|
||||||
|
<button class="btn btn-primary btn-sm" id="btn-mem-resize" onclick="resizeMemory()" style="margin-left:0.5em;">Átméretezés</button>
|
||||||
|
<span id="mem-status-msg" style="margin-left:0.5em; display:none;"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Jelenlegi memória</span>
|
||||||
|
<span class="settings-value mono">{{if .MemoryProcMB}}{{.MemoryProcMB}} MB{{else}}n/a{{end}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="settings-card-desc">A kiszolgáló ügynök jelenleg nem elérhető — próbáld meg később.</p>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<p class="settings-card-desc">A memória átméretezéséhez a kiszolgáló rendszerfrissítése szükséges.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="dialog-root"></div>
|
<div id="dialog-root"></div>
|
||||||
<script>
|
<script>
|
||||||
// Light overlay dialog (D1) — replaces the native blocking browser dialogs (same texts).
|
// Light overlay dialog (D1) — replaces the native blocking browser dialogs (same texts).
|
||||||
@@ -220,6 +262,55 @@ function doTriggerUpdate() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resizeMemory() {
|
||||||
|
var input = document.getElementById('mem-input');
|
||||||
|
var target = parseInt(input.value, 10);
|
||||||
|
var current = parseInt(input.getAttribute('data-current'), 10);
|
||||||
|
if (isNaN(target) || target <= 0) {
|
||||||
|
showMemMsg('Érvénytelen memória érték', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target < current) {
|
||||||
|
openDialog({title:'Memória csökkentése', confirmLabel:'Csökkentés',
|
||||||
|
message:'A memória csökkentése hatással lehet a futó alkalmazások teljesítményére. Biztosan folytatod?',
|
||||||
|
onConfirm:function(){ doResizeMemory(target); }});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doResizeMemory(target);
|
||||||
|
}
|
||||||
|
function doResizeMemory(target) {
|
||||||
|
var btn = document.getElementById('btn-mem-resize');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Átméretezés...';
|
||||||
|
fetch('/api/system/memory/resize', {
|
||||||
|
method:'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
||||||
|
body: JSON.stringify({memory_mb: target})
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.ok) {
|
||||||
|
showMemMsg(data.error || data.message || 'Kész.', false);
|
||||||
|
setTimeout(function(){ location.reload(); }, 1200);
|
||||||
|
} else {
|
||||||
|
showMemMsg(data.error || 'A művelet nem sikerült', true);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Átméretezés';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
showMemMsg('Kapcsolódási hiba', true);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Átméretezés';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function showMemMsg(text, isErr) {
|
||||||
|
var msg = document.getElementById('mem-status-msg');
|
||||||
|
if (!msg) return;
|
||||||
|
msg.textContent = text;
|
||||||
|
msg.style.color = isErr ? 'var(--danger, #c0392b)' : 'var(--text-2)';
|
||||||
|
msg.style.display = 'inline';
|
||||||
|
}
|
||||||
|
|
||||||
function pollUntilBack() {
|
function pollUntilBack() {
|
||||||
var iv = setInterval(function() {
|
var iv = setInterval(function() {
|
||||||
fetch('/api/health')
|
fetch('/api/health')
|
||||||
|
|||||||
Reference in New Issue
Block a user