diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index ee55d599..a391a031 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -235,6 +235,18 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) + // Setup status endpoint (always returns needs_setup: false in normal mode) + // This is used by the frontend to detect when the service has restarted after setup + r.GET("/setup/status", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "data": gin.H{ + "needs_setup": false, + "step": "completed", + }, + }) + }) + // API v1 v1 := r.Group("/api/v1") { diff --git a/backend/internal/setup/handler.go b/backend/internal/setup/handler.go index 5565539d..a8e73a03 100644 --- a/backend/internal/setup/handler.go +++ b/backend/internal/setup/handler.go @@ -2,11 +2,15 @@ package setup import ( "fmt" + "log" "net/http" "net/mail" + "os/exec" "regexp" + "runtime" "strings" "sync" + "time" "sub2api/internal/pkg/response" @@ -337,8 +341,41 @@ func install(c *gin.Context) { return } + // Schedule service restart in background after sending response + // This ensures the client receives the success response before the service restarts + go func() { + // Wait a moment to ensure the response is sent + time.Sleep(500 * time.Millisecond) + triggerServiceRestart() + }() + response.Success(c, gin.H{ - "message": "Installation completed successfully", + "message": "Installation completed successfully. Service will restart automatically.", "restart": true, }) } + +// triggerServiceRestart attempts to restart the service via systemd +// This is called after setup completes to switch from setup mode to normal mode +func triggerServiceRestart() { + if runtime.GOOS != "linux" { + log.Println("Service restart: not on Linux, manual restart required") + return + } + + log.Println("Setup completed, triggering service restart...") + + // Try direct systemctl first (works if running as root or with proper permissions) + cmd := exec.Command("systemctl", "restart", "sub2api") + if err := cmd.Run(); err != nil { + // Try with sudo (requires NOPASSWD sudoers entry) + sudoCmd := exec.Command("sudo", "systemctl", "restart", "sub2api") + if sudoErr := sudoCmd.Run(); sudoErr != nil { + log.Printf("Service restart failed: %v (sudo also failed: %v)", err, sudoErr) + log.Println("Please restart the service manually: sudo systemctl restart sub2api") + return + } + } + + log.Println("Service restart initiated successfully") +} diff --git a/backend/internal/web/embed.go b/backend/internal/web/embed.go index 93cfe763..da5ba167 100644 --- a/backend/internal/web/embed.go +++ b/backend/internal/web/embed.go @@ -10,7 +10,7 @@ import ( "github.com/gin-gonic/gin" ) -//go:embed dist/* +//go:embed all:dist var frontendFS embed.FS // ServeEmbeddedFrontend returns a Gin handler that serves embedded frontend assets diff --git a/frontend/src/views/setup/SetupWizardView.vue b/frontend/src/views/setup/SetupWizardView.vue index 4d63118e..ee7bb640 100644 --- a/frontend/src/views/setup/SetupWizardView.vue +++ b/frontend/src/views/setup/SetupWizardView.vue @@ -214,12 +214,18 @@
- + + + + +

Installation completed!

-

Please restart the service to apply changes.

+

+ {{ serviceReady ? 'Redirecting to login page...' : 'Service is restarting, please wait...' }} +

@@ -290,6 +296,7 @@ const dbConnected = ref(false); const redisConnected = ref(false); const installing = ref(false); const confirmPassword = ref(''); +const serviceReady = ref(false); // Get current server port from browser location (set by install.sh) const getCurrentPort = (): number => { @@ -390,6 +397,8 @@ async function performInstall() { try { await install(formData); installSuccess.value = true; + // Start polling for service restart + waitForServiceRestart(); } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } }; message?: string }; errorMessage.value = err.response?.data?.detail || err.message || 'Installation failed'; @@ -397,4 +406,52 @@ async function performInstall() { installing.value = false; } } + +// Wait for service to restart and become available +async function waitForServiceRestart() { + const maxAttempts = 30; // 30 attempts, ~30 seconds max + const interval = 1000; // 1 second between attempts + + // Wait a moment for the service to start restarting + await new Promise(resolve => setTimeout(resolve, 2000)); + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + // Try to access the health endpoint + const response = await fetch('/health', { + method: 'GET', + cache: 'no-store' + }); + + if (response.ok) { + // Service is up, check if setup is no longer needed + const statusResponse = await fetch('/setup/status', { + method: 'GET', + cache: 'no-store' + }); + + if (statusResponse.ok) { + const data = await statusResponse.json(); + // If needs_setup is false, service has restarted in normal mode + if (data.data && !data.data.needs_setup) { + serviceReady.value = true; + // Redirect to login page after a short delay + setTimeout(() => { + window.location.href = '/login'; + }, 1500); + return; + } + } + } + } catch { + // Service not ready yet, continue polling + } + + await new Promise(resolve => setTimeout(resolve, interval)); + } + + // If we reach here, service didn't restart in time + // Show a message to refresh manually + errorMessage.value = 'Service restart is taking longer than expected. Please refresh the page manually.'; +}