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
This commit is contained in:
@@ -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,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"},
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user