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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user