YMhut Setup
Setup frontend is not built. Run npm install && npm run build in web/setup.
` + index + `
package web import ( "encoding/json" "errors" "net/http" "path/filepath" "strings" "time" "ymhut-box/server/unified-management/internal/config" "ymhut-box/server/unified-management/internal/db" ) type setupRouter struct { cfg *config.Config } type setupRequest struct { Provider string `json:"provider"` BaseURL string `json:"baseUrl"` SQLitePath string `json:"sqlitePath"` MySQLDSN string `json:"mysqlDsn"` MySQL config.MySQLInput `json:"mysql"` } func NewSetupRouter(cfg *config.Config) http.Handler { return withSecurity(&setupRouter{cfg: cfg}) } func (r *setupRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { path := cleanPath(req.URL.Path) switch { case path == "/" || path == "/setup": r.serveSetup(w, req) case strings.HasPrefix(path, "/setup/assets/"): serveStaticAsset(w, req, r.cfg.SetupWebDir, "setup/dist", strings.TrimPrefix(path, "/setup/")) case path == "/api/setup/status": writeJSON(w, http.StatusOK, r.status()) case path == "/api/setup/database/test": r.handleDatabaseTest(w, req) case path == "/api/setup/complete": r.handleComplete(w, req) default: if strings.HasPrefix(path, "/api/") { writeError(w, http.StatusServiceUnavailable, "SETUP_REQUIRED", errors.New("system setup is required")) return } http.Redirect(w, req, "/setup", http.StatusFound) } } func (r *setupRouter) status() map[string]any { return map[string]any{ "ok": true, "initialized": r.cfg.Initialized, "baseDir": ".", "configPath": relativeToBase(r.cfg.BaseDir, r.cfg.ConfigPath), "defaults": map[string]any{ "provider": firstNonEmpty(r.cfg.Database.Provider, "sqlite"), "sqlitePath": relativeToBase(r.cfg.BaseDir, r.cfg.Database.SQLitePath), "mysqlDsn": config.MaskDSN(r.cfg.Database.MySQLDSN), "baseUrl": r.cfg.BaseURL, }, } } func (r *setupRouter) handleDatabaseTest(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", errors.New("POST required")) return } next, body, err := r.decodeSetupDatabase(req) if err != nil { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err) return } started := time.Now() if err := db.TestDatabase(next); err != nil { writeError(w, http.StatusBadGateway, "DATABASE_TEST_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "provider": next.Provider, "latencyMs": time.Since(started).Milliseconds(), "maskedDsn": maskedDatabaseTarget(r.cfg.BaseDir, next), "normalized": map[string]any{ "provider": next.Provider, "baseUrl": firstNonEmpty(body.BaseURL, r.cfg.BaseURL), "sqlitePath": relativeToBase(r.cfg.BaseDir, next.SQLitePath), "mysqlDsn": config.MaskDSN(next.MySQLDSN), }, }) } func (r *setupRouter) handleComplete(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", errors.New("POST required")) return } nextDB, body, err := r.decodeSetupDatabase(req) if err != nil { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err) return } if err := db.TestDatabase(nextDB); err != nil { writeError(w, http.StatusBadGateway, "DATABASE_TEST_FAILED", err) return } next := *r.cfg next.Initialized = true next.BaseURL = firstNonEmpty(strings.TrimSpace(body.BaseURL), next.BaseURL) next.Database = nextDB if strings.EqualFold(next.Database.Provider, "mysql") { next.Database.FailoverEnabled = true next.Database.HotSyncEnabled = true } store, err := db.Open(&next) if err != nil { writeError(w, http.StatusInternalServerError, "DATABASE_OPEN_FAILED", err) return } if err := store.EnsureDefaultAdmin(req.Context()); err != nil { _ = store.Close() writeError(w, http.StatusInternalServerError, "ADMIN_INIT_FAILED", err) return } _ = store.Close() if err := config.Save(&next); err != nil { writeError(w, http.StatusInternalServerError, "CONFIG_SAVE_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "initialized": true, "message": "Setup completed. Restart the service, then open /admin/login."}) } func (r *setupRouter) decodeSetupDatabase(req *http.Request) (config.DatabaseConfig, setupRequest, error) { var body setupRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { return config.DatabaseConfig{}, body, err } incoming := config.DatabaseConfig{ Provider: body.Provider, SQLitePath: body.SQLitePath, MySQLDSN: body.MySQLDSN, MySQLHost: body.MySQL.Host, MySQLPort: body.MySQL.Port, MySQLDatabase: body.MySQL.Database, MySQLUser: body.MySQL.Username, MySQLPassword: body.MySQL.Password, } next, err := config.NormalizeDatabase(r.cfg.BaseDir, r.cfg.Database, incoming, false) return next, body, err } func (r *setupRouter) serveSetup(w http.ResponseWriter, req *http.Request) { index := filepath.Join(r.cfg.SetupWebDir, "index.html") if tryServeDiskFile(w, req, r.cfg.SetupWebDir, "index.html") { return } if serveEmbeddedFile(w, req, "setup/dist/index.html") { return } w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(`
Setup frontend is not built. Run npm install && npm run build in web/setup.
` + index + `