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:
2026-07-17 19:10:00 +02:00
parent f900c83eed
commit 3286c7faaf
10 changed files with 688 additions and 1 deletions
+95
View File
@@ -454,6 +454,101 @@ func (c *Client) GuestReboot(ctx context.Context) error {
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).
type SwapResult struct {
Status string `json:"status"` // "swapping"