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 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-02-03 11:38:28 +03:00
parent e2d056e7cb
commit 0eb6730a55
3 changed files with 74 additions and 12 deletions

View File

@@ -7,7 +7,10 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path/filepath"
"runtime"
"strconv" "strconv"
"syscall" "syscall"
"time" "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) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
@@ -172,7 +187,7 @@ func main() {
func setConfigDefaults(cfg *config.Config) { func setConfigDefaults(cfg *config.Config) {
if cfg.Server.Host == "" { if cfg.Server.Host == "" {
cfg.Server.Host = "0.0.0.0" cfg.Server.Host = "127.0.0.1"
} }
if cfg.Server.Port == 0 { if cfg.Server.Port == 0 {
cfg.Server.Port = 8080 cfg.Server.Port = 8080
@@ -211,7 +226,8 @@ func runSetupMode(local *localdb.LocalDB) {
restartSig := make(chan struct{}, 1) restartSig := make(chan struct{}, 1)
// In setup mode, we don't have a connection manager yet (will restart after setup) // 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 { if err != nil {
slog.Error("failed to create setup handler", "error", err) slog.Error("failed to create setup handler", "error", err)
os.Exit(1) os.Exit(1)
@@ -221,7 +237,8 @@ func runSetupMode(local *localdb.LocalDB) {
router := gin.New() router := gin.New()
router.Use(gin.Recovery()) router.Use(gin.Recovery())
router.Static("/static", "web/static") staticPath := filepath.Join("web", "static")
router.Static("/static", staticPath)
// Setup routes only // Setup routes only
router.GET("/", func(c *gin.Context) { 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("starting setup mode server", "address", addr)
slog.Info("open http://localhost:8080/setup to configure database connection")
srv := &http.Server{ srv := &http.Server{
Addr: addr, 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) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 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) // Local-first configuration service (replaces old ConfigurationService)
configService := services.NewLocalConfigurationService(local, syncService, quoteService, isOnline) configService := services.NewLocalConfigurationService(local, syncService, quoteService, isOnline)
// Use filepath.Join for cross-platform path compatibility
templatesPath := filepath.Join("web", "templates")
// Handlers // Handlers
componentHandler := handlers.NewComponentHandler(componentService, local) componentHandler := handlers.NewComponentHandler(componentService, local)
quoteHandler := handlers.NewQuoteHandler(quoteService) quoteHandler := handlers.NewQuoteHandler(quoteService)
exportHandler := handlers.NewExportHandler(exportService, configService, componentService) exportHandler := handlers.NewExportHandler(exportService, configService, componentService)
pricingHandler := handlers.NewPricingHandler(mariaDB, pricingService, alertService, componentRepo, priceRepo, statsRepo) pricingHandler := handlers.NewPricingHandler(mariaDB, pricingService, alertService, componentRepo, priceRepo, statsRepo)
pricelistHandler := handlers.NewPricelistHandler(pricelistService, local) 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 { if err != nil {
return nil, nil, fmt.Errorf("creating sync handler: %w", err) return nil, nil, fmt.Errorf("creating sync handler: %w", err)
} }
// Setup handler (for reconfiguration) - no restart signal in normal mode // 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 { if err != nil {
return nil, nil, fmt.Errorf("creating setup handler: %w", err) return nil, nil, fmt.Errorf("creating setup handler: %w", err)
} }
// Web handler (templates) // Web handler (templates)
webHandler, err := handlers.NewWebHandler("web/templates", componentService) webHandler, err := handlers.NewWebHandler(templatesPath, componentService)
if err != nil { if err != nil {
return nil, nil, err 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.CORS())
router.Use(middleware.OfflineDetector(connMgr, local)) router.Use(middleware.OfflineDetector(connMgr, local))
// Static files // Static files (use filepath.Join for Windows compatibility)
router.Static("/static", "web/static") staticPath := filepath.Join("web", "static")
router.Static("/static", staticPath)
// Health check // Health check
router.GET("/health", func(c *gin.Context) { 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 // DB status endpoint
router.GET("/api/db-status", func(c *gin.Context) { router.GET("/api/db-status", func(c *gin.Context) {
var lotCount, lotLogCount, metadataCount int64 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 { func requestLogger() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
start := time.Now() start := time.Now()

View File

@@ -2,7 +2,7 @@
# Copy this file to config.yaml and update values # Copy this file to config.yaml and update values
server: server:
host: "0.0.0.0" host: "127.0.0.1" # Use 0.0.0.0 to listen on all interfaces
port: 8080 port: 8080
mode: "release" # debug | release mode: "release" # debug | release
read_timeout: "30s" read_timeout: "30s"

View File

@@ -106,7 +106,7 @@ func Load(path string) (*Config, error) {
func (c *Config) setDefaults() { func (c *Config) setDefaults() {
if c.Server.Host == "" { if c.Server.Host == "" {
c.Server.Host = "0.0.0.0" c.Server.Host = "127.0.0.1"
} }
if c.Server.Port == 0 { if c.Server.Port == 0 {
c.Server.Port = 8080 c.Server.Port = 8080