Compare commits

..

2 Commits

Author SHA1 Message Date
admin 7109e88e07 docs: REPORT.md for v0.81.0 (server-reboot button / banner retirement)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PsnU2ASocYrvzqE82YDYW
2026-06-23 18:47:28 +02:00
admin 242b835a19 v0.81.0: retire drive-activation banner; add standalone "Kiszolgáló újraindítása" button
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PsnU2ASocYrvzqE82YDYW
2026-06-23 18:46:31 +02:00
8 changed files with 142 additions and 121 deletions
+24
View File
@@ -1,5 +1,29 @@
## Changelog ## 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) ### 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 - Follow-up to v0.78/0.79. The storage disk card displayed the drive's **raw** host PVE mount
(`/mnt/<name>`) — which doesn't exist inside the guest — instead of the **stable** in-guest path (`/mnt/<name>`) — which doesn't exist inside the guest — instead of the **stable** in-guest path
+38 -52
View File
@@ -1,60 +1,46 @@
# REPORT — controller v0.80.0: disk card shows + acts on the stable path # REPORT — retire drive-activation banner; add standalone "Kiszolgáló újraindítása" button
## Baseline **Repo:** felhom-controller · **Baseline:** `main` @ `7cce19797` (v0.80.0) → **v0.81.0**
- felhom-controller `main` @ `9596c8e`, **v0.79.0 → v0.80.0**. Trunk-based, `main`. **Commit:** `242b835` (code + CHANGELOG + README). **Date:** 2026-06-23.
- Follow-up to v0.78.0 (stable-path registration) + v0.79.0 (registered badge on stable path).
## Problem (operator-reported, screenshot) ## Why
The storage disk card still displayed each external drive's **raw** host PVE mount `/mnt/<name>` In the intermediary-mount model an enrolled drive binds **live** into the running guest (agent
(`usb · /dev/sdc1 · /mnt/felhom-flash`). Inside the guest/controller the drive lives at the **stable** `disks.go` — no `pct set -mpN`, no slot, no reboot), so the "… meghajtó aktiválásra vár /
path `/mnt/felhom-drives/<name>` (the agent's `guest_path`) — which is what the registry, app `HDD_PATH`, Újraindítás most (~30 mp)" banner was an obsolete relic of the old per-drive reboot model. It was
and FileBrowser use; the raw path doesn't even exist in the guest. Two latent bugs rode on the same also effectively **dead since v0.78**: `pendingActivationDrives` keyed `attached` by the agent's RAW
mismatch: the Leválasztás/Törlés buttons posted the **raw** path, so `handleStorageEject`/`handleStorageWipe` `MountPath` but compared it to the now-STABLE `sp.Path`. Retired it; added a deliberate full-server
would unmount the drive but `RemoveStoragePath(raw)` would miss the **stable** registry entry (orphan); restart affordance in its place (sibling to the controller-only restart).
and the impact warning `/api/storage/impact?where=raw` found no apps (HDD_PATH is the stable path).
## Fix ## Files changed
**`controller/internal/web/templates/settings.html`** (disk-view JS): - `controller/internal/web/storage_handlers.go`
- Card sub-line now shows `regKey(d)` (= `d.guest_path || d.mount_path`) — the stable in-guest path. - Removed dead `pendingActivationDrives` helper + the now-unused `internal/system` import.
- `actions()` passes the **stable** path to `confirmEject`/`confirmWipe` (handlers map it to raw for the - Renamed `handleStorageActivate``HandleServerReboot`; split out testable `serverReboot(w,r,agent)` core.
agent via `agentWhere()` AND deregister the stable registry entry). `registerDrive` keeps the **raw** - Added `GuestReboot(ctx) error` to the `diskAgent` interface (`*agentapi.Client` already satisfies it).
`mount_path` (its agent guest-attach operates on raw; `handleStorageRegister` maps to stable). - Removed the `/api/storage/activate` case from `ServeStorageAPI` (→ 404).
- `confirmEject`/`confirmWipe` derive the type-to-confirm name from the **basename** - `controller/cmd/controller/main.go` — mounted `/api/server/reboot` (`RequireAuth`+`CsrfProtect`) next to the storage route.
(`where.split('/').filter(Boolean).pop()`) so it still equals the server's `path.Base(where)` check - `controller/internal/web/handlers.go` — removed the `data["PendingDrives"]` feed.
when `where` is the stable path. - `controller/internal/web/templates/settings.html` — removed the `{{if .PendingDrives}}` banner block and `window.activatePendingDrives`; added the **"Kiszolgáló újraindítása"** settings card + `restartServer()` JS (reuses the existing `pollRestart()` loop).
- `controller/internal/web/storage_handlers_test.go``mockAgent` gained `GuestReboot`; new test.
- `CHANGELOG.md` (v0.81.0 entry), `controller/README.md` (full-server-restart section).
**`controller/internal/web/storage_handlers.go`** (`handleStorageWipe`): ### Note on naming
- The agent eject now uses `agentWhere(req.Where)` (was `req.Where` verbatim), matching The HTTP handler is **exported** (`HandleServerReboot`), not the lowercase name in the spec snippet:
`handleStorageEject`. So a stable registered path maps to the raw mount for the agent while `cmd/controller/main.go` wires it cross-package, and every web handler mounted there is exported. The
`RemoveStoragePath(req.Where)` deregisters the stable entry. Agent op is the identical raw path as unexported `serverReboot` core carries the logic and is what the test exercises (same split-out pattern
before; only the registry bookkeeping is corrected. The format step is unchanged (keyed on `device`). as `runStorageInit`).
Agent-facing operations (eject unmount, format) produce the IDENTICAL raw paths/device as before — only ## Test
the controller's display + its own registry bookkeeping/impact lookups now use the stable path. `TestHandleServerReboot_CallsGuestReboot` (`storage_handlers_test.go`): a fake `diskAgent` asserts
`GuestReboot` is invoked **exactly once** and the response is **202** with `{ok:true, rebooting:true}`.
## Tests ```
- `go build ./... && go vet ./... && go test ./...`**all green** (incl. `TestTemplatesParse`). go build ./... → BUILD_OK
- `handleStorageWipe`'s new mapping rides on `agentWhere`, already covered by `TestAgentWhere` go vet ./... → VET_OK
(stable→raw, raw idempotent). JS changes covered by template parse (no JS unit harness — existing go test ./... → ok (internal/web 1.640s, new test PASS); all packages ok
pattern). ```
## Deploy (v0.80.0 → guest 9201) Grep confirmed **zero** remaining references to `PendingDrives`, `pendingActivationDrives`,
- Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.80.0` on 192.168.0.180; deployed to 9201 `activatePendingDrives`, `activate-drives-btn`, `/api/storage/activate`, `handleStorageActivate`.
(pull → `/etc/felhom-controller-image` → restart bootstrap unit); `docker ps``:0.80.0 Up (healthy)`.
## Verification (served page JS + data the card renders from) ## Deploy + live verification
- Served `/settings` JS (the disk card is built client-side from these): sub-line _To be filled after the build/deploy + §9 verification (banner gone, button present, route 200/404, regression)._
`… +(d.mount_path?' · '+esc(regKey(d)):'')` (stable); `confirmEject('+reg+')` and
`confirmWipe('+dev+','+reg+')` (stable); `registerDrive('+mpRaw+')` (raw). Confirms v0.80.0 shipped.
- `GET /api/disks`: `felhom-flash → guest_path=/mnt/felhom-drives/felhom-flash`,
`felhom-usb → guest_path=/mnt/felhom-drives/felhom-usb` ⇒ the card renders the stable path and the
eject/wipe buttons carry it.
- Regression: `[PASS] Storage paths: 2 connected, 0 disconnected`; `[PASS] System data path: /mnt/sys_drive`.
- NOT live-clicked: the destructive eject/wipe buttons (would unmount/format the demo drives). Verified
the path-mapping logic by inspection + the agent ops are unchanged (raw via `agentWhere`/device); the
rendering is pure client-side JS over the confirmed-correct inputs (bridge not attached this session).
## Observations (recorded, NOT acted on)
- `pendingActivationDrives` still keys off the agent's raw `MountPath` (the "~30 mp újraindítás" banner);
obsolete in the live-activating intermediary model — key off `GuestPath`/`BoundUnderParent` or retire.
- Free-form settings storage-add (`handlers.go`) registers a user-typed path verbatim (advanced flow).
+2
View File
@@ -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. **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`) #### App Telemetry (`internal/metrics/telemetry.go`, `internal/metrics/logscanner.go`, `internal/report/telemetry.go`)
Each report push now includes per-app telemetry data: Each report push now includes per-app telemetry data:
+3
View File
@@ -726,6 +726,9 @@ 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))))
// 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 // 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))))
-3
View File
@@ -903,9 +903,6 @@ func (s *Server) settingsData() map[string]interface{} {
storageViews = append(storageViews, view) storageViews = append(storageViews, view)
} }
data["StoragePaths"] = storageViews 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 // Recovery info for emergency section
data["RetrievalPassword"] = s.settings.GetRetrievalPassword() data["RetrievalPassword"] = s.settings.GetRetrievalPassword()
+17 -47
View File
@@ -17,7 +17,6 @@ import (
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings" "gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks" "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 // 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) EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error)
Decommission(ctx context.Context, where string) (agentapi.DecommissionResult, error) Decommission(ctx context.Context, where string) (agentapi.DecommissionResult, error)
GuestAttach(ctx context.Context, where string) error GuestAttach(ctx context.Context, where string) error
GuestReboot(ctx context.Context) error
} }
// mountNameRe is the safe `/mnt/<name>` component (DNS-ish: letters, digits, _ , -). // mountNameRe is the safe `/mnt/<name>` 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 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 // 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. // and restores Schedulable so its apps' "missing storage" indicator clears. Returns true if it acted.
func (s *Server) reEnrollClearMarker(where string) (bool, error) { 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) s.handleStorageImpact(w, r)
case r.URL.Path == "/api/storage/register" && r.Method == http.MethodPost: case r.URL.Path == "/api/storage/register" && r.Method == http.MethodPost:
s.handleStorageRegister(w, r) 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: case r.URL.Path == "/api/storage/migrate" && r.Method == http.MethodPost:
s.handleStorageMigrate(w, r) s.handleStorageMigrate(w, r)
case r.URL.Path == "/api/storage/migrate-app" && r.Method == http.MethodPost: 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}) 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 // HandleServerReboot reboots the whole guest (server) as a deliberate maintenance action — the
// reboots detached + returns 202; this controller restarts with the guest, so the caller's response // standalone "Kiszolgáló újraindítása" affordance, a sibling to the controller-only restart
// may be cut short — the UI handles that and reloads after the restart window. // (/api/selfrestart). The agent reboots detached + returns 202; this controller restarts with the
func (s *Server) handleStorageActivate(w http.ResponseWriter, r *http.Request) { // 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() agent, err := s.agentClient()
if err != nil { if err != nil {
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil) writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
return 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 { 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) writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
return 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}) writeDiskJSON(w, http.StatusAccepted, true, "", map[string]any{"rebooting": true})
} }
@@ -40,6 +40,8 @@ type mockAgent struct {
formatCalls []formatCall formatCalls []formatCall
guestAttachCalls []string guestAttachCalls []string
decommissionCalls []string decommissionCalls []string
guestRebootCalls int
guestRebootErr error
} }
type assignCall struct{ uuid, where, fstype string } 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) m.guestAttachCalls = append(m.guestAttachCalls, where)
return nil return nil
} }
func (m *mockAgent) GuestReboot(context.Context) error {
m.guestRebootCalls++
return m.guestRebootErr
}
func testServer(t *testing.T) *Server { func testServer(t *testing.T) *Server {
t.Helper() 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) { func TestFSUUIDForDevice(t *testing.T) {
disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{ disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
{BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"}, {BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"},
+20 -19
View File
@@ -408,15 +408,6 @@ function pollUntilBack() {
<a href="/settings/storage/attach" class="btn btn-sm btn-outline">🔗 Meglévő meghajtó csatolása</a> <a href="/settings/storage/attach" class="btn btn-sm btn-outline">🔗 Meglévő meghajtó csatolása</a>
</div> </div>
{{if .PendingDrives}}
<div class="alert alert-warning" style="margin-top:1rem">
<strong>{{len .PendingDrives}} meghajtó aktiválásra vár.</strong>
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.
<div style="margin-top:.6rem"><button class="btn btn-sm btn-primary" id="activate-drives-btn" onclick="activatePendingDrives()">Újraindítás most (~30 mp)</button></div>
<div id="activate-result" style="margin-top:.5rem"></div>
</div>
{{end}}
<div style="margin-top:1.5rem"> <div style="margin-top:1.5rem">
<h4 style="margin-bottom:.25rem">Meghajtók (ügynök nézet)</h4> <h4 style="margin-bottom:.25rem">Meghajtók (ügynök nézet)</h4>
<p class="form-hint" style="margin-bottom:.75rem">A host-ügynök által észlelt meghajtók élő nézete. A meghajtó <strong>szerepkörét</strong> 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.</p> <p class="form-hint" style="margin-bottom:.75rem">A host-ügynök által észlelt meghajtók élő nézete. A meghajtó <strong>szerepkörét</strong> 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.</p>
@@ -564,16 +555,6 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
location.reload(); location.reload();
}catch(e){ alert('Hiba: '+e.message); } }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='<span class="form-hint">Újraindítás folyamatban… az oldal automatikusan újratöltődik.</span>';
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){ window.confirmEject=function(where){
// The confirm name is the drive BASENAME (matches the server's path.Base(where) check); `where` may // The confirm name is the drive BASENAME (matches the server's path.Base(where) check); `where` may
// be the stable /mnt/felhom-drives/<name> path, so strip the whole directory, not just the /mnt/ prefix. // be the stable /mnt/felhom-drives/<name> path, so strip the whole directory, not just the /mnt/ prefix.
@@ -1144,6 +1125,16 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Vezérlő újraindítása</button> <button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Vezérlő újraindítása</button>
</div> </div>
<!-- Section: Full server (guest) restart — a deliberate maintenance affordance, sibling to the controller restart -->
<div class="settings-card">
<h3>Kiszolgáló újraindítása</h3>
<p class="settings-card-desc">
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor használja, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő.
</p>
<div id="server-restart-status"></div>
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">Kiszolgáló újraindítása</button>
</div>
<script> <script>
function restartController() { function restartController() {
if (!confirm('Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.')) return; if (!confirm('Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.')) return;
@@ -1155,6 +1146,16 @@ function restartController() {
.then(function(){ pollRestart(0); }) .then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless .catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
} }
function restartServer() {
if (!confirm('Biztosan újraindítja a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.')) return;
var btn = document.getElementById('btn-restart-server');
var status = document.getElementById('server-restart-status');
if (btn) btn.disabled = true;
if (status) status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… a vezérlőpult néhány másodperc múlva újratölt.</div>';
fetch('/api/server/reboot', { method: 'POST', headers: csrfHeaders() })
.then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless
}
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 =