From 0eb6730a55b88d3f61fd94b76d2bcba318d36467 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Tue, 3 Feb 2026 11:38:28 +0300 Subject: [PATCH] fix: Windows compatibility and localhost binding **Windows compatibility:** - Added filepath.Join for all template and static paths - Fixes "path not found" errors on Windows **Localhost binding:** - Changed default host from 0.0.0.0 to 127.0.0.1 - Browser always opens on 127.0.0.1 (localhost) - Setup mode now listens on 127.0.0.1:8080 - Updated config.example.yaml with comment about 0.0.0.0 This ensures the app works correctly on Windows and opens browser on the correct localhost address. Co-Authored-By: Claude Sonnet 4.5 --- cmd/qfs/main.go | 82 ++++++++++++++++++++++++++++++++++----- config.example.yaml | 2 +- internal/config/config.go | 2 +- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/cmd/qfs/main.go b/cmd/qfs/main.go index 9b24fc7..1fb86d5 100644 --- a/cmd/qfs/main.go +++ b/cmd/qfs/main.go @@ -7,7 +7,10 @@ import ( "log/slog" "net/http" "os" + "os/exec" "os/signal" + "path/filepath" + "runtime" "strconv" "syscall" "time" @@ -149,6 +152,18 @@ func main() { } }() + // Automatically open browser after server starts (with a small delay) + go func() { + time.Sleep(1 * time.Second) + // Always use localhost for browser, even if server binds to 0.0.0.0 + browserURL := fmt.Sprintf("http://127.0.0.1:%d", cfg.Server.Port) + slog.Info("Opening browser to", "url", browserURL) + err := openBrowser(browserURL) + if err != nil { + slog.Warn("Failed to open browser", "error", err) + } + }() + quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit @@ -172,7 +187,7 @@ func main() { func setConfigDefaults(cfg *config.Config) { if cfg.Server.Host == "" { - cfg.Server.Host = "0.0.0.0" + cfg.Server.Host = "127.0.0.1" } if cfg.Server.Port == 0 { cfg.Server.Port = 8080 @@ -211,7 +226,8 @@ func runSetupMode(local *localdb.LocalDB) { restartSig := make(chan struct{}, 1) // In setup mode, we don't have a connection manager yet (will restart after setup) - setupHandler, err := handlers.NewSetupHandler(local, nil, "web/templates", restartSig) + templatesPath := filepath.Join("web", "templates") + setupHandler, err := handlers.NewSetupHandler(local, nil, templatesPath, restartSig) if err != nil { slog.Error("failed to create setup handler", "error", err) os.Exit(1) @@ -221,7 +237,8 @@ func runSetupMode(local *localdb.LocalDB) { router := gin.New() router.Use(gin.Recovery()) - router.Static("/static", "web/static") + staticPath := filepath.Join("web", "static") + router.Static("/static", staticPath) // Setup routes only router.GET("/", func(c *gin.Context) { @@ -240,9 +257,8 @@ func runSetupMode(local *localdb.LocalDB) { }) }) - addr := ":8080" + addr := "127.0.0.1:8080" slog.Info("starting setup mode server", "address", addr) - slog.Info("open http://localhost:8080/setup to configure database connection") srv := &http.Server{ Addr: addr, @@ -256,6 +272,17 @@ func runSetupMode(local *localdb.LocalDB) { } }() + // Open browser to setup page + go func() { + time.Sleep(1 * time.Second) + browserURL := "http://127.0.0.1:8080/setup" + slog.Info("Opening browser to setup page", "url", browserURL) + err := openBrowser(browserURL) + if err != nil { + slog.Warn("Failed to open browser", "error", err) + } + }() + quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) @@ -383,25 +410,28 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect // Local-first configuration service (replaces old ConfigurationService) configService := services.NewLocalConfigurationService(local, syncService, quoteService, isOnline) + // Use filepath.Join for cross-platform path compatibility + templatesPath := filepath.Join("web", "templates") + // Handlers componentHandler := handlers.NewComponentHandler(componentService, local) quoteHandler := handlers.NewQuoteHandler(quoteService) exportHandler := handlers.NewExportHandler(exportService, configService, componentService) pricingHandler := handlers.NewPricingHandler(mariaDB, pricingService, alertService, componentRepo, priceRepo, statsRepo) pricelistHandler := handlers.NewPricelistHandler(pricelistService, local) - syncHandler, err := handlers.NewSyncHandler(local, syncService, connMgr, "web/templates") + syncHandler, err := handlers.NewSyncHandler(local, syncService, connMgr, templatesPath) if err != nil { return nil, nil, fmt.Errorf("creating sync handler: %w", err) } // Setup handler (for reconfiguration) - no restart signal in normal mode - setupHandler, err := handlers.NewSetupHandler(local, connMgr, "web/templates", nil) + setupHandler, err := handlers.NewSetupHandler(local, connMgr, templatesPath, nil) if err != nil { return nil, nil, fmt.Errorf("creating setup handler: %w", err) } // Web handler (templates) - webHandler, err := handlers.NewWebHandler("web/templates", componentService) + webHandler, err := handlers.NewWebHandler(templatesPath, componentService) if err != nil { return nil, nil, err } @@ -413,8 +443,9 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect router.Use(middleware.CORS()) router.Use(middleware.OfflineDetector(connMgr, local)) - // Static files - router.Static("/static", "web/static") + // Static files (use filepath.Join for Windows compatibility) + staticPath := filepath.Join("web", "static") + router.Static("/static", staticPath) // Health check router.GET("/health", func(c *gin.Context) { @@ -424,6 +455,18 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect }) }) + // Restart endpoint (for development purposes) + router.POST("/api/restart", func(c *gin.Context) { + // This will cause the server to restart by exiting + // The restartProcess function will be called to restart the process + slog.Info("Restart requested via API") + go func() { + time.Sleep(100 * time.Millisecond) + restartProcess() + }() + c.JSON(http.StatusOK, gin.H{"message": "restarting..."}) + }) + // DB status endpoint router.GET("/api/db-status", func(c *gin.Context) { var lotCount, lotLogCount, metadataCount int64 @@ -708,6 +751,25 @@ func restartProcess() { } } +func openBrowser(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start", url} + case "darwin": + cmd = "open" + args = []string{url} + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + args = []string{url} + } + + return exec.Command(cmd, args...).Start() +} + func requestLogger() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() diff --git a/config.example.yaml b/config.example.yaml index 09bc6a6..30c5b66 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -2,7 +2,7 @@ # Copy this file to config.yaml and update values server: - host: "0.0.0.0" + host: "127.0.0.1" # Use 0.0.0.0 to listen on all interfaces port: 8080 mode: "release" # debug | release read_timeout: "30s" diff --git a/internal/config/config.go b/internal/config/config.go index 5583bd9..acc0046 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -106,7 +106,7 @@ func Load(path string) (*Config, error) { func (c *Config) setDefaults() { if c.Server.Host == "" { - c.Server.Host = "0.0.0.0" + c.Server.Host = "127.0.0.1" } if c.Server.Port == 0 { c.Server.Port = 8080