From 242b835a19b5428a71bc2c413572588861f70092 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 23 Jun 2026 18:46:31 +0200 Subject: [PATCH] =?UTF-8?q?v0.81.0:=20retire=20drive-activation=20banner;?= =?UTF-8?q?=20add=20standalone=20"Kiszolg=C3=A1l=C3=B3=20=C3=BAjraind?= =?UTF-8?q?=C3=ADt=C3=A1sa"=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the intermediary-mount model an enrolled drive binds live into the running guest (no reboot), so the "… meghajtó aktiválásra vár / Újraindítás most" banner was an obsolete relic — also dead since v0.78 (pendingActivationDrives keyed on the raw MountPath vs the now-stable sp.Path). Removed the banner block + activatePendingDrives JS (settings.html), the PendingDrives feed (handlers.go), and the dead pendingActivationDrives helper + its unused internal/system import. Renamed handleStorageActivate -> HandleServerReboot (split out a testable serverReboot core), removed the /api/storage/activate case, and mounted the handler at the new non-storage route /api/server/reboot (RequireAuth+CsrfProtect). The agent GuestReboot primitive is reused unchanged. Added the standalone "Kiszolgáló újraindítása" settings card (sibling to the controller-only "Vezérlő újraindítása"), reusing the pollRestart() loop. Test: TestHandleServerReboot_CallsGuestReboot (fake diskAgent asserts GuestReboot invoked once + 202). diskAgent/mockAgent gained GuestReboot. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_017PsnU2ASocYrvzqE82YDYW --- CHANGELOG.md | 24 +++++++ controller/README.md | 2 + controller/cmd/controller/main.go | 3 + controller/internal/web/handlers.go | 3 - controller/internal/web/storage_handlers.go | 64 +++++-------------- .../internal/web/storage_handlers_test.go | 38 +++++++++++ .../internal/web/templates/settings.html | 39 +++++------ 7 files changed, 104 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c156ed..224022e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ ## Changelog +### v0.81.0 — retire the drive-activation banner; add a standalone "Kiszolgáló újraindítása" button (2026-06-23) +- **Removed the obsolete drive-activation banner.** In the intermediary-mount model an enrolled drive + binds **live** into the running guest (agent `disks.go` — no `pct set -mpN`, no slot, no reboot), so + the "… meghajtó aktiválásra vár / Újraindítás most (~30 mp)" banner was a relic of the old per-drive + reboot model. It was also effectively dead since v0.78 (`pendingActivationDrives` keyed `attached` by + the agent's RAW `MountPath` but compared it to the now-STABLE `sp.Path`). Removed: the + `{{if .PendingDrives}}` banner block + `window.activatePendingDrives` JS (`settings.html`), the + `data["PendingDrives"]` feed (`handlers.go`), and the dead `pendingActivationDrives` helper + + its now-unused `internal/system` import (`storage_handlers.go`). +- **Repointed the reboot endpoint to a non-storage route.** Renamed `handleStorageActivate` → + `HandleServerReboot` and split out a testable `serverReboot` core (mirrors `runStorageInit`); removed + the `/api/storage/activate` case from `ServeStorageAPI`; mounted the handler at the new + `/api/server/reboot` (same `RequireAuth` + `CsrfProtect`) in `cmd/controller/main.go`. The agent + `GuestReboot` primitive is reused unchanged. (`/api/storage/activate` now returns 404.) + *Note:* the handler is exported (`HandleServerReboot`) because `cmd/controller/main.go` wires it + cross-package — same convention as every other web handler mounted there. +- **Added the standalone "Kiszolgáló újraindítása" settings card.** A deliberate full-server (guest) + restart affordance, a sibling to the existing "Vezérlő újraindítása" controller-only restart. + New `settings-card` + `restartServer()` JS (reuses the existing `pollRestart()` loop) in + `settings.html`; posts to `/api/server/reboot`. +- **Test:** `TestHandleServerReboot_CallsGuestReboot` (`storage_handlers_test.go`) — a fake `diskAgent` + asserts `GuestReboot` is invoked exactly once and the response is 202 `{ok:true, rebooting:true}`. + `diskAgent`/`mockAgent` gained `GuestReboot`. Green gate: `go build ./... && go vet ./... && go test ./...`. + ### v0.80.0 — disk card: show + act on the stable path, not the raw host mount (2026-06-23) - Follow-up to v0.78/0.79. The storage disk card displayed the drive's **raw** host PVE mount (`/mnt/`) — which doesn't exist inside the guest — instead of the **stable** in-guest path diff --git a/controller/README.md b/controller/README.md index f58df6a..e17588a 100644 --- a/controller/README.md +++ b/controller/README.md @@ -1271,6 +1271,8 @@ Bearer token authentication, 3-attempt retry with 5-second backoff. Push status **Manual restart (v0.70.0):** `POST /api/selfrestart` (session auth + CSRF via the `/api/` mount) runs the same helper — surfaced as the **"Vezérlő újraindítása"** button on the settings page (confirm → POST → poll `GET /` every 2 s → reload), so a customer can recover the controller without rebooting the whole guest. +**Full-server restart (v0.81.0):** `POST /api/server/reboot` (session auth + CSRF) reboots the **whole guest** via the host agent's `GuestReboot` primitive (`HandleServerReboot` in `internal/web/storage_handlers.go`, delegating to the testable `serverReboot` core; agent reboots detached + returns 202). Surfaced as a separate **"Kiszolgáló újraindítása"** settings card alongside the controller-only restart, reusing the same `pollRestart()` reload loop. It replaces the retired drive-activation banner (v0.81.0): in the intermediary-mount model an enrolled drive binds **live** into the running guest, so storage no longer needs a reboot to activate — this button is purely a deliberate full-system restart. + #### App Telemetry (`internal/metrics/telemetry.go`, `internal/metrics/logscanner.go`, `internal/report/telemetry.go`) Each report push now includes per-app telemetry data: diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index 07ad62f..e8374e9 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -726,6 +726,9 @@ func main() { 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). mux.Handle("/api/storage/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeStorageAPI)))) + // 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. + mux.Handle("/api/server/reboot", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.HandleServerReboot)))) // 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. mux.Handle("/api/guest-backup/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeBackupAPI)))) diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index c857113..f657ea0 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -903,9 +903,6 @@ func (s *Server) settingsData() map[string]interface{} { storageViews = append(storageViews, view) } data["StoragePaths"] = storageViews - // Drives enrolled but not yet activated in the guest (slice 10 P2): they need the user-triggered - // "Újraindítás most" to take effect (the host-side live inject is blocked on an unprivileged guest). - data["PendingDrives"] = s.pendingActivationDrives() // Recovery info for emergency section data["RetrievalPassword"] = s.settings.GetRetrievalPassword() diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go index c6edac4..f4dfdab 100644 --- a/controller/internal/web/storage_handlers.go +++ b/controller/internal/web/storage_handlers.go @@ -17,7 +17,6 @@ import ( "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" - "gitea.dooplex.hu/admin/felhom-controller/internal/system" ) // Guided storage provisioning (rebuilt on the agent-delegated disk model). The controller is a thin @@ -35,6 +34,7 @@ type diskAgent interface { EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error) Decommission(ctx context.Context, where string) (agentapi.DecommissionResult, error) GuestAttach(ctx context.Context, where string) error + GuestReboot(ctx context.Context) error } // mountNameRe is the safe `/mnt/` component (DNS-ish: letters, digits, _ , -). @@ -164,44 +164,6 @@ func (s *Server) runStorageAttach(ctx context.Context, agent diskAgent, device, return storageInitResult{Registered: true, Where: stable}, nil } -// pendingActivationDrives returns registered storage paths that are NOT yet live-mounted in this -// container but whose backing drive the agent reports present+attached — enrolled drives waiting for -// the guest restart that activates their bind (slice 10 P2; the host-side live inject is blocked on an -// unprivileged guest). The customer activates them with the "Újraindítás most" button (one restart -// batches all). Best-effort: agent unreachable → none. -func (s *Server) pendingActivationDrives() []string { - paths := s.settings.GetStoragePaths() - if len(paths) == 0 { - return nil - } - agent, err := s.agentClient() - if err != nil { - return nil - } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - resp, err := agent.Disks(ctx) - if err != nil { - return nil - } - attached := map[string]bool{} - for _, d := range resp.Disks { - if d.MountPath != "" && d.State == "attached" { - attached[d.MountPath] = true - } - } - var pending []string - for _, sp := range paths { - if sp.Decommissioned { - continue - } - if attached[sp.Path] && !system.IsMountPoint(sp.Path) { - pending = append(pending, sp.Path) - } - } - return pending -} - // reEnrollClearMarker un-retires a re-plugged decommissioned drive (Change 4): clears the soft marker // and restores Schedulable so its apps' "missing storage" indicator clears. Returns true if it acted. func (s *Server) reEnrollClearMarker(where string) (bool, error) { @@ -283,8 +245,6 @@ func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) { s.handleStorageImpact(w, r) case r.URL.Path == "/api/storage/register" && r.Method == http.MethodPost: s.handleStorageRegister(w, r) - case r.URL.Path == "/api/storage/activate" && r.Method == http.MethodPost: - s.handleStorageActivate(w, r) case r.URL.Path == "/api/storage/migrate" && r.Method == http.MethodPost: s.handleStorageMigrate(w, r) case r.URL.Path == "/api/storage/migrate-app" && r.Method == http.MethodPost: @@ -643,21 +603,31 @@ func (s *Server) handleStorageWipe(w http.ResponseWriter, r *http.Request) { writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"device": req.Device, "wiped": fr.Formatted, "durable_id": fr.DurableID}) } -// handleStorageActivate reboots the guest to activate pending drive binds (slice 10 P2). The agent -// reboots detached + returns 202; this controller restarts with the guest, so the caller's response -// may be cut short — the UI handles that and reloads after the restart window. -func (s *Server) handleStorageActivate(w http.ResponseWriter, r *http.Request) { +// HandleServerReboot reboots the whole guest (server) as a deliberate maintenance action — the +// standalone "Kiszolgáló újraindítása" affordance, a sibling to the controller-only restart +// (/api/selfrestart). The agent reboots detached + returns 202; this controller restarts with the +// guest, so the caller's response may be cut short — the UI handles that and reloads after the +// restart window. (It reuses the agent GuestReboot primitive that previously backed the now-retired +// drive-activation banner; in the intermediary-mount model a drive binds live, so no reboot is needed +// to activate storage — this button is purely a full-server restart.) +func (s *Server) HandleServerReboot(w http.ResponseWriter, r *http.Request) { agent, err := s.agentClient() if err != nil { writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil) return } + s.serverReboot(w, r, agent) +} + +// serverReboot is the testable core of HandleServerReboot: invoke the agent's guest reboot and return +// the 202 envelope (or the agent error). Split out so it can be exercised with a fake diskAgent. +func (s *Server) serverReboot(w http.ResponseWriter, r *http.Request, agent diskAgent) { if err := agent.GuestReboot(r.Context()); err != nil { - s.logger.Printf("[ERROR] [web] guest reboot (activate pending drives) failed: %v", err) + s.logger.Printf("[ERROR] [web] server reboot (guest restart) failed: %v", err) writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil) return } - s.logger.Printf("[WARN] [web] guest restart requested to activate pending drive binds") + s.logger.Printf("[WARN] [web] full server (guest) restart requested by operator") writeDiskJSON(w, http.StatusAccepted, true, "", map[string]any{"rebooting": true}) } diff --git a/controller/internal/web/storage_handlers_test.go b/controller/internal/web/storage_handlers_test.go index 8fd8ee2..f9a4f08 100644 --- a/controller/internal/web/storage_handlers_test.go +++ b/controller/internal/web/storage_handlers_test.go @@ -40,6 +40,8 @@ type mockAgent struct { formatCalls []formatCall guestAttachCalls []string decommissionCalls []string + guestRebootCalls int + guestRebootErr error } type assignCall struct{ uuid, where, fstype string } @@ -71,6 +73,10 @@ func (m *mockAgent) GuestAttach(_ context.Context, where string) error { m.guestAttachCalls = append(m.guestAttachCalls, where) return nil } +func (m *mockAgent) GuestReboot(context.Context) error { + m.guestRebootCalls++ + return m.guestRebootErr +} func testServer(t *testing.T) *Server { t.Helper() @@ -255,6 +261,38 @@ func TestHandleStorageRegister_RegistersStablePath(t *testing.T) { } } +// TestHandleServerReboot_CallsGuestReboot exercises the standalone "Kiszolgáló újraindítása" core: +// it must invoke the agent's GuestReboot exactly once and return the 202 envelope. (The HTTP handler +// HandleServerReboot delegates to this core after building the live agent client — same split-out +// pattern as runStorageInit, so the fake diskAgent can be injected here.) +func TestHandleServerReboot_CallsGuestReboot(t *testing.T) { + s := testServer(t) + agent := &mockAgent{} + req := httptest.NewRequest(http.MethodPost, "/api/server/reboot", nil) + rr := httptest.NewRecorder() + + s.serverReboot(rr, req, agent) + + if agent.guestRebootCalls != 1 { + t.Fatalf("expected GuestReboot invoked exactly once, got %d", agent.guestRebootCalls) + } + if rr.Code != http.StatusAccepted { + t.Fatalf("expected 202 Accepted, got %d (body %s)", rr.Code, rr.Body.String()) + } + var resp struct { + OK bool `json:"ok"` + Data struct { + Rebooting bool `json:"rebooting"` + } `json:"data"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode response: %v", err) + } + if !resp.OK || !resp.Data.Rebooting { + t.Fatalf("expected {ok:true, data.rebooting:true}, got %+v", resp) + } +} + func TestFSUUIDForDevice(t *testing.T) { disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{ {BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"}, diff --git a/controller/internal/web/templates/settings.html b/controller/internal/web/templates/settings.html index bad17e6..ddfc08b 100644 --- a/controller/internal/web/templates/settings.html +++ b/controller/internal/web/templates/settings.html @@ -408,15 +408,6 @@ function pollUntilBack() { 🔗 Meglévő meghajtó csatolása - {{if .PendingDrives}} -
- {{len .PendingDrives}} meghajtó aktiválásra vár. - Az újonnan csatolt adatmeghajtók a vendég rövid (~30 mp) újraindítása után válnak elérhetővé az alkalmazások számára. -
-
-
- {{end}} -

Meghajtók (ügynök nézet)

A host-ügynök által észlelt meghajtók élő nézete. A meghajtó szerepkörét az ügynök saját vizsgálattal állapítja meg: a rendszer- és biztonsági-mentés meghajtók védettek (csak operátori aláírással módosíthatók), a felhasználói adatmeghajtókat Ön kezeli.

@@ -564,16 +555,6 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}} location.reload(); }catch(e){ alert('Hiba: '+e.message); } }; - // Activate pending drive binds by rebooting the guest (~30s). The reboot takes the controller down - // too, so the fetch may not resolve — we reload after the restart window regardless. - window.activatePendingDrives=function(){ - if(!confirm('A vendég újraindul (~30 másodperc). Eközben az alkalmazások és a vezérlőpult rövid időre nem elérhetők. Folytatja?')) return; - var btn=document.getElementById('activate-drives-btn'); var out=document.getElementById('activate-result'); - if(btn) btn.disabled=true; - if(out) out.innerHTML='Újraindítás folyamatban… az oldal automatikusan újratöltődik.'; - fetch('/api/storage/activate',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders())}).catch(function(){}); - setTimeout(function(){location.reload();}, 45000); - }; window.confirmEject=function(where){ // The confirm name is the drive BASENAME (matches the server's path.Base(where) check); `where` may // be the stable /mnt/felhom-drives/ path, so strip the whole directory, not just the /mnt/ prefix. @@ -1144,6 +1125,16 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
+ +
+

Kiszolgáló újraindítása

+

+ 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álja, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő. +

+
+ +
+