diff --git a/cmd/picoclaw/cmd_gateway.go b/cmd/picoclaw/cmd_gateway.go index 29de28883..b76c072de 100644 --- a/cmd/picoclaw/cmd_gateway.go +++ b/cmd/picoclaw/cmd_gateway.go @@ -193,20 +193,32 @@ func gatewayCmd() { cfg.Dashboard.Password = dashboard.GeneratePassword() configPath := getConfigPath() if saveErr := config.SaveConfig(configPath, cfg); saveErr != nil { - logger.ErrorCF("dashboard", "Failed to save generated password", map[string]any{"error": saveErr.Error()}) + logger.ErrorCF( + "dashboard", + "Failed to save generated password", + map[string]any{"error": saveErr.Error()}, + ) } fmt.Printf("🔑 Dashboard password generated: %s\n", cfg.Dashboard.Password) } dashConfigPath := getConfigPath() dashboard.Mount(healthServer, cfg, agentLoop, channelManager, dashConfigPath) - fmt.Printf("✓ Dashboard available at http://%s:%d/dashboard\n", cfg.Gateway.Host, cfg.Gateway.Port) + fmt.Printf( + "✓ Dashboard available at http://%s:%d/dashboard\n", + cfg.Gateway.Host, + cfg.Gateway.Port, + ) } go func() { if err := healthServer.Start(); err != nil && err != http.ErrServerClosed { logger.ErrorCF("health", "Health server error", map[string]any{"error": err.Error()}) } }() - fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port) + fmt.Printf( + "✓ Health endpoints available at http://%s:%d/health and /ready\n", + cfg.Gateway.Host, + cfg.Gateway.Port, + ) go agentLoop.Run(ctx) @@ -239,7 +251,15 @@ func setupCronTool( cronService := cron.NewCronService(cronStorePath, nil) // Create and register CronTool - cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg) + cronTool := tools.NewCronTool( + cronService, + agentLoop, + msgBus, + workspace, + restrict, + execTimeout, + cfg, + ) agentLoop.RegisterTool(cronTool) // Set the onJob handler diff --git a/pkg/dashboard/api.go b/pkg/dashboard/api.go index 9179f22cd..cb5d74857 100644 --- a/pkg/dashboard/api.go +++ b/pkg/dashboard/api.go @@ -11,7 +11,12 @@ import ( "github.com/sipeed/picoclaw/pkg/config" ) -func statusHandler(cfg *config.Config, al *agent.AgentLoop, cm *channels.Manager, startTime time.Time) http.HandlerFunc { +func statusHandler( + cfg *config.Config, + al *agent.AgentLoop, + cm *channels.Manager, + startTime time.Time, +) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { info := al.GetStartupInfo() channelStatus := cm.GetStatus() diff --git a/pkg/dashboard/crud_agents.go b/pkg/dashboard/crud_agents.go index 1422fce67..58b61e541 100644 --- a/pkg/dashboard/crud_agents.go +++ b/pkg/dashboard/crud_agents.go @@ -13,7 +13,12 @@ import ( "github.com/sipeed/picoclaw/pkg/health" ) -func registerAgentsCRUD(srv *health.Server, cfg *config.Config, configPath string, auth func(http.HandlerFunc) http.HandlerFunc) { +func registerAgentsCRUD( + srv *health.Server, + cfg *config.Config, + configPath string, + auth func(http.HandlerFunc) http.HandlerFunc, +) { srv.HandleFunc("/dashboard/fragments/agent-edit", auth(fragmentAgentEdit(cfg))) srv.HandleFunc("/dashboard/fragments/agent-add", auth(fragmentAgentAdd())) srv.HandleFunc("/dashboard/crud/agents/create", auth(agentCreateHandler(cfg, configPath))) @@ -256,7 +261,11 @@ func agentCreateHandler(cfg *config.Config, configPath string) http.HandlerFunc if instructions := r.FormValue("instructions"); strings.TrimSpace(instructions) != "" { ws := resolveAgentWorkspace(cfg, &agent) if err := writeAgentInstructions(ws, instructions); err != nil { - jsonError(w, "agent created but failed to write AGENT.md: "+err.Error(), http.StatusInternalServerError) + jsonError( + w, + "agent created but failed to write AGENT.md: "+err.Error(), + http.StatusInternalServerError, + ) return } } @@ -318,7 +327,11 @@ func agentUpdateHandler(cfg *config.Config, configPath string) http.HandlerFunc instructions := r.FormValue("instructions") ws := resolveAgentWorkspace(cfg, found) if err := writeAgentInstructions(ws, instructions); err != nil { - jsonError(w, "agent updated but failed to write AGENT.md: "+err.Error(), http.StatusInternalServerError) + jsonError( + w, + "agent updated but failed to write AGENT.md: "+err.Error(), + http.StatusInternalServerError, + ) return } diff --git a/pkg/dashboard/crud_channels.go b/pkg/dashboard/crud_channels.go index e4b7c2d50..b8c2ed59b 100644 --- a/pkg/dashboard/crud_channels.go +++ b/pkg/dashboard/crud_channels.go @@ -11,7 +11,12 @@ import ( "github.com/sipeed/picoclaw/pkg/health" ) -func registerChannelsCRUD(srv *health.Server, cfg *config.Config, configPath string, auth func(http.HandlerFunc) http.HandlerFunc) { +func registerChannelsCRUD( + srv *health.Server, + cfg *config.Config, + configPath string, + auth func(http.HandlerFunc) http.HandlerFunc, +) { srv.HandleFunc("/dashboard/fragments/channel-edit", auth(fragmentChannelEdit(cfg))) srv.HandleFunc("/dashboard/crud/channels/update", auth(channelUpdateHandler(cfg, configPath))) srv.HandleFunc("/dashboard/crud/channels/toggle", auth(channelToggleHandler(cfg, configPath))) @@ -100,7 +105,11 @@ func channelEditFormHTML(name string, cfg *config.Config) string { allowFrom = ch.AllowFrom fields = textField("ws_url", "WebSocket URL", ch.WSUrl) + textField("access_token", "Access Token", ch.AccessToken) + - textField("reconnect_interval", "Reconnect Interval", strconv.Itoa(ch.ReconnectInterval)) + textField( + "reconnect_interval", + "Reconnect Interval", + strconv.Itoa(ch.ReconnectInterval), + ) default: return "" } @@ -160,7 +169,11 @@ func fragmentChannelEdit(cfg *config.Config) http.HandlerFunc { if html == "" { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `
Unknown channel: %s
`, template.HTMLEscapeString(name)) + fmt.Fprintf( + w, + `Unknown channel: %s
`, + template.HTMLEscapeString(name), + ) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -263,7 +276,11 @@ func channelUpdateHandler(cfg *config.Config, configPath string) http.HandlerFun configMu.Unlock() w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `Unknown channel: %s
`, template.HTMLEscapeString(name)) + fmt.Fprintf( + w, + `Unknown channel: %s
`, + template.HTMLEscapeString(name), + ) return } configMu.Unlock() @@ -272,7 +289,11 @@ func channelUpdateHandler(cfg *config.Config, configPath string) http.HandlerFun if err := saveConfig(configPath, cfg); err != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, `Failed to save: %s
`, template.HTMLEscapeString(err.Error())) + fmt.Fprintf( + w, + `Failed to save: %s
`, + template.HTMLEscapeString(err.Error()), + ) return } } @@ -321,7 +342,11 @@ func channelToggleHandler(cfg *config.Config, configPath string) http.HandlerFun configMu.Unlock() w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, `Unknown channel: %s
`, template.HTMLEscapeString(name)) + fmt.Fprintf( + w, + `Unknown channel: %s
`, + template.HTMLEscapeString(name), + ) return } configMu.Unlock() @@ -330,7 +355,11 @@ func channelToggleHandler(cfg *config.Config, configPath string) http.HandlerFun if err := saveConfig(configPath, cfg); err != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, `Failed to save: %s
`, template.HTMLEscapeString(err.Error())) + fmt.Fprintf( + w, + `Failed to save: %s
`, + template.HTMLEscapeString(err.Error()), + ) return } } diff --git a/pkg/dashboard/crud_models.go b/pkg/dashboard/crud_models.go index ca657d313..726899d8f 100644 --- a/pkg/dashboard/crud_models.go +++ b/pkg/dashboard/crud_models.go @@ -10,7 +10,12 @@ import ( "github.com/sipeed/picoclaw/pkg/health" ) -func registerModelsCRUD(srv *health.Server, cfg *config.Config, configPath string, auth func(http.HandlerFunc) http.HandlerFunc) { +func registerModelsCRUD( + srv *health.Server, + cfg *config.Config, + configPath string, + auth func(http.HandlerFunc) http.HandlerFunc, +) { srv.HandleFunc("/dashboard/fragments/model-edit", auth(fragmentModelEdit(cfg))) srv.HandleFunc("/dashboard/fragments/model-add", auth(fragmentModelAdd())) srv.HandleFunc("/dashboard/crud/models/create", auth(modelCreateHandler(cfg, configPath))) @@ -159,7 +164,11 @@ func modelCreateHandler(cfg *config.Config, configPath string) http.HandlerFunc if err := saveConfig(configPath, cfg); err != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, `Failed to save: %s
`, template.HTMLEscapeString(err.Error())) + fmt.Fprintf( + w, + `Failed to save: %s
`, + template.HTMLEscapeString(err.Error()), + ) return } } @@ -200,7 +209,11 @@ func modelUpdateHandler(cfg *config.Config, configPath string) http.HandlerFunc if err := saveConfig(configPath, cfg); err != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, `Failed to save: %s
`, template.HTMLEscapeString(err.Error())) + fmt.Fprintf( + w, + `Failed to save: %s
`, + template.HTMLEscapeString(err.Error()), + ) return } } @@ -237,7 +250,11 @@ func modelDeleteHandler(cfg *config.Config, configPath string) http.HandlerFunc if err := saveConfig(configPath, cfg); err != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, `Failed to save: %s
`, template.HTMLEscapeString(err.Error())) + fmt.Fprintf( + w, + `Failed to save: %s
`, + template.HTMLEscapeString(err.Error()), + ) return } } diff --git a/pkg/dashboard/crud_settings.go b/pkg/dashboard/crud_settings.go index aed5924b9..af0104561 100644 --- a/pkg/dashboard/crud_settings.go +++ b/pkg/dashboard/crud_settings.go @@ -10,11 +10,23 @@ import ( "github.com/sipeed/picoclaw/pkg/health" ) -func registerSettingsCRUD(srv *health.Server, cfg *config.Config, configPath string, currentPassword string, auth func(http.HandlerFunc) http.HandlerFunc) { +func registerSettingsCRUD( + srv *health.Server, + cfg *config.Config, + configPath string, + currentPassword string, + auth func(http.HandlerFunc) http.HandlerFunc, +) { srv.HandleFunc("/dashboard/fragments/settings", auth(fragmentSettings(cfg, currentPassword))) - srv.HandleFunc("/dashboard/crud/settings/password", auth(passwordChangeHandler(cfg, configPath))) + srv.HandleFunc( + "/dashboard/crud/settings/password", + auth(passwordChangeHandler(cfg, configPath)), + ) srv.HandleFunc("/dashboard/crud/settings/gateway", auth(gatewayUpdateHandler(cfg, configPath))) - srv.HandleFunc("/dashboard/crud/settings/heartbeat", auth(heartbeatUpdateHandler(cfg, configPath))) + srv.HandleFunc( + "/dashboard/crud/settings/heartbeat", + auth(heartbeatUpdateHandler(cfg, configPath)), + ) srv.HandleFunc("/dashboard/crud/settings/devices", auth(devicesUpdateHandler(cfg, configPath))) } diff --git a/pkg/dashboard/crud_settings_test.go b/pkg/dashboard/crud_settings_test.go index 3f7ffd041..4a4ca527c 100644 --- a/pkg/dashboard/crud_settings_test.go +++ b/pkg/dashboard/crud_settings_test.go @@ -22,7 +22,11 @@ func TestPasswordChange(t *testing.T) { form.Set("current_password", "oldpassword") form.Set("new_password", "newpassword123") - req := httptest.NewRequest(http.MethodPost, "/dashboard/crud/settings/password", strings.NewReader(form.Encode())) + req := httptest.NewRequest( + http.MethodPost, + "/dashboard/crud/settings/password", + strings.NewReader(form.Encode()), + ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() @@ -68,7 +72,11 @@ func TestPasswordChangeWrongCurrent(t *testing.T) { form.Set("current_password", "wrongpassword") form.Set("new_password", "newpassword123") - req := httptest.NewRequest(http.MethodPost, "/dashboard/crud/settings/password", strings.NewReader(form.Encode())) + req := httptest.NewRequest( + http.MethodPost, + "/dashboard/crud/settings/password", + strings.NewReader(form.Encode()), + ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() @@ -99,7 +107,11 @@ func TestPasswordChangeTooShort(t *testing.T) { form.Set("current_password", "oldpassword") form.Set("new_password", "short") - req := httptest.NewRequest(http.MethodPost, "/dashboard/crud/settings/password", strings.NewReader(form.Encode())) + req := httptest.NewRequest( + http.MethodPost, + "/dashboard/crud/settings/password", + strings.NewReader(form.Encode()), + ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() @@ -129,7 +141,11 @@ func TestGatewayUpdate(t *testing.T) { form.Set("host", "127.0.0.1") form.Set("port", "9090") - req := httptest.NewRequest(http.MethodPost, "/dashboard/crud/settings/gateway", strings.NewReader(form.Encode())) + req := httptest.NewRequest( + http.MethodPost, + "/dashboard/crud/settings/gateway", + strings.NewReader(form.Encode()), + ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() @@ -174,7 +190,11 @@ func TestGatewayUpdateBadPort(t *testing.T) { form.Set("host", "localhost") form.Set("port", tt.port) - req := httptest.NewRequest(http.MethodPost, "/dashboard/crud/settings/gateway", strings.NewReader(form.Encode())) + req := httptest.NewRequest( + http.MethodPost, + "/dashboard/crud/settings/gateway", + strings.NewReader(form.Encode()), + ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() @@ -197,7 +217,11 @@ func TestHeartbeatUpdate(t *testing.T) { form.Set("enabled", "on") form.Set("interval", "15") - req := httptest.NewRequest(http.MethodPost, "/dashboard/crud/settings/heartbeat", strings.NewReader(form.Encode())) + req := httptest.NewRequest( + http.MethodPost, + "/dashboard/crud/settings/heartbeat", + strings.NewReader(form.Encode()), + ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() @@ -230,7 +254,11 @@ func TestDevicesUpdate(t *testing.T) { form.Set("enabled", "on") form.Set("monitor_usb", "on") - req := httptest.NewRequest(http.MethodPost, "/dashboard/crud/settings/devices", strings.NewReader(form.Encode())) + req := httptest.NewRequest( + http.MethodPost, + "/dashboard/crud/settings/devices", + strings.NewReader(form.Encode()), + ) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go index 5b5e40b2a..06bab8d77 100644 --- a/pkg/dashboard/dashboard.go +++ b/pkg/dashboard/dashboard.go @@ -15,7 +15,13 @@ import ( "github.com/sipeed/picoclaw/pkg/logger" ) -func Mount(srv *health.Server, cfg *config.Config, al *agent.AgentLoop, cm *channels.Manager, configPath ...string) { +func Mount( + srv *health.Server, + cfg *config.Config, + al *agent.AgentLoop, + cm *channels.Manager, + configPath ...string, +) { startTime := time.Now() broker := NewBroker() password := cfg.Dashboard.Password @@ -34,7 +40,10 @@ func Mount(srv *health.Server, cfg *config.Config, al *agent.AgentLoop, cm *chan logger.ErrorCF("dashboard", "Failed to create sub FS", map[string]any{"error": err.Error()}) return } - srv.Handle("/dashboard/static/", http.StripPrefix("/dashboard/static/", http.FileServer(http.FS(staticFS)))) + srv.Handle( + "/dashboard/static/", + http.StripPrefix("/dashboard/static/", http.FileServer(http.FS(staticFS))), + ) // Auth routes (public) srv.HandleFunc("/dashboard/login", func(w http.ResponseWriter, r *http.Request) { @@ -115,7 +124,12 @@ var funcMap = template.FuncMap{ "extractProvider": extractProvider, } -func fragmentStatus(cfg *config.Config, al *agent.AgentLoop, cm *channels.Manager, startTime time.Time) http.HandlerFunc { +func fragmentStatus( + cfg *config.Config, + al *agent.AgentLoop, + cm *channels.Manager, + startTime time.Time, +) http.HandlerFunc { const tmpl = `