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
### 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/<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
- felhom-controller `main` @ `9596c8e`, **v0.79.0 → v0.80.0**. Trunk-based, `main`.
- Follow-up to v0.78.0 (stable-path registration) + v0.79.0 (registered badge on stable path).
**Repo:** felhom-controller · **Baseline:** `main` @ `7cce19797` (v0.80.0) → **v0.81.0**
**Commit:** `242b835` (code + CHANGELOG + README). **Date:** 2026-06-23.
## Problem (operator-reported, screenshot)
The storage disk card still displayed each external drive's **raw** host PVE mount `/mnt/<name>`
(`usb · /dev/sdc1 · /mnt/felhom-flash`). Inside the guest/controller the drive lives at the **stable**
path `/mnt/felhom-drives/<name>` (the agent's `guest_path`) — which is what the registry, app `HDD_PATH`,
and FileBrowser use; the raw path doesn't even exist in the guest. Two latent bugs rode on the same
mismatch: the Leválasztás/Törlés buttons posted the **raw** path, so `handleStorageEject`/`handleStorageWipe`
would unmount the drive but `RemoveStoragePath(raw)` would miss the **stable** registry entry (orphan);
and the impact warning `/api/storage/impact?where=raw` found no apps (HDD_PATH is the stable path).
## Why
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 an obsolete 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`. Retired it; added a deliberate full-server
restart affordance in its place (sibling to the controller-only restart).
## Fix
**`controller/internal/web/templates/settings.html`** (disk-view JS):
- Card sub-line now shows `regKey(d)` (= `d.guest_path || d.mount_path`) — the stable in-guest path.
- `actions()` passes the **stable** path to `confirmEject`/`confirmWipe` (handlers map it to raw for the
agent via `agentWhere()` AND deregister the stable registry entry). `registerDrive` keeps the **raw**
`mount_path` (its agent guest-attach operates on raw; `handleStorageRegister` maps to stable).
- `confirmEject`/`confirmWipe` derive the type-to-confirm name from the **basename**
(`where.split('/').filter(Boolean).pop()`) so it still equals the server's `path.Base(where)` check
when `where` is the stable path.
## Files changed
- `controller/internal/web/storage_handlers.go`
- Removed dead `pendingActivationDrives` helper + the now-unused `internal/system` import.
- Renamed `handleStorageActivate``HandleServerReboot`; split out testable `serverReboot(w,r,agent)` core.
- Added `GuestReboot(ctx) error` to the `diskAgent` interface (`*agentapi.Client` already satisfies it).
- Removed the `/api/storage/activate` case from `ServeStorageAPI` (→ 404).
- `controller/cmd/controller/main.go` — mounted `/api/server/reboot` (`RequireAuth`+`CsrfProtect`) next to the storage route.
- `controller/internal/web/handlers.go` — removed the `data["PendingDrives"]` feed.
- `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`):
- The agent eject now uses `agentWhere(req.Where)` (was `req.Where` verbatim), matching
`handleStorageEject`. So a stable registered path maps to the raw mount for the agent while
`RemoveStoragePath(req.Where)` deregisters the stable entry. Agent op is the identical raw path as
before; only the registry bookkeeping is corrected. The format step is unchanged (keyed on `device`).
### Note on naming
The HTTP handler is **exported** (`HandleServerReboot`), not the lowercase name in the spec snippet:
`cmd/controller/main.go` wires it cross-package, and every web handler mounted there is exported. The
unexported `serverReboot` core carries the logic and is what the test exercises (same split-out pattern
as `runStorageInit`).
Agent-facing operations (eject unmount, format) produce the IDENTICAL raw paths/device as before — only
the controller's display + its own registry bookkeeping/impact lookups now use the stable path.
## Test
`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`).
- `handleStorageWipe`'s new mapping rides on `agentWhere`, already covered by `TestAgentWhere`
(stable→raw, raw idempotent). JS changes covered by template parse (no JS unit harness — existing
pattern).
```
go build ./... → BUILD_OK
go vet ./... → VET_OK
go test ./... → ok (internal/web 1.640s, new test PASS); all packages ok
```
## Deploy (v0.80.0 → guest 9201)
- Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.80.0` on 192.168.0.180; deployed to 9201
(pull → `/etc/felhom-controller-image` → restart bootstrap unit); `docker ps``:0.80.0 Up (healthy)`.
Grep confirmed **zero** remaining references to `PendingDrives`, `pendingActivationDrives`,
`activatePendingDrives`, `activate-drives-btn`, `/api/storage/activate`, `handleStorageActivate`.
## Verification (served page JS + data the card renders from)
- Served `/settings` JS (the disk card is built client-side from these): sub-line
`… +(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).
## Deploy + live verification
_To be filled after the build/deploy + §9 verification (banner gone, button present, route 200/404, regression)._
+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.
**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:
+3
View File
@@ -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))))
-3
View File
@@ -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()
+17 -47
View File
@@ -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/<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
}
// 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})
}
@@ -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"},
+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>
</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">
<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>
@@ -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='<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){
// 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.
@@ -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>
</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>
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;
@@ -1155,6 +1146,16 @@ function restartController() {
.then(function(){ pollRestart(0); })
.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) {
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
document.getElementById('restart-status').innerHTML =