Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion chatapps/slack/apphome/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ func (h *Handler) HandleHomeOpened(ctx context.Context, event *HomeOpenedEvent)
},
)
if err != nil {
// not_enabled means the App Home feature is not enabled in the Slack App config.
// This is a configuration issue, not a system failure — degrade gracefully.
if isAppHomeNotEnabled(err) {
h.logger.Info("App Home feature not enabled, skipping Home Tab update",
"user", event.User)
return nil
}
h.logger.Error("Failed to publish Home Tab view",
"user", event.User,
"error", err)
Expand Down Expand Up @@ -282,5 +289,27 @@ func (h *Handler) HandleHomeRefresh(ctx context.Context, userID string) error {
View: *view,
},
)
return err
if err != nil {
// not_enabled means the App Home feature is not enabled in the Slack App config.
if isAppHomeNotEnabled(err) {
h.logger.Info("App Home feature not enabled, skipping Home Tab refresh",
"user", userID)
return nil
}
h.logger.Error("Failed to refresh Home Tab view",
"user", userID,
"error", err)
return fmt.Errorf("publish view: %w", err)
}
return nil
}

// isAppHomeNotEnabled checks if the error indicates App Home is not enabled
// in the Slack App configuration.
func isAppHomeNotEnabled(err error) bool {
if err == nil {
return false
}
errStr := strings.ToLower(err.Error())
return strings.Contains(errStr, "not_enabled")
}
14 changes: 13 additions & 1 deletion chatapps/slack/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,19 @@ func (a *Adapter) AppendStream(ctx context.Context, channelID, messageTS, conten

_, _, err := a.client.AppendStreamContext(ctx, channelID, messageTS, options...)
if err != nil {
a.Logger().Error("AppendStream failed", "channel_id", channelID, "message_ts", messageTS, "error", err)
// message_not_in_streaming_state is a recoverable condition handled by the
// streaming writer's fallback mechanism — log at WARN to avoid alarming operators.
if isStreamStateError(err) {
a.Logger().Warn("AppendStream failed (stream expired)",
"channel_id", channelID,
"message_ts", messageTS,
"error", err)
} else {
a.Logger().Error("AppendStream failed",
"channel_id", channelID,
"message_ts", messageTS,
"error", err)
}
return fmt.Errorf("append stream: %w", err)
}

Expand Down
43 changes: 42 additions & 1 deletion cmd/hotplexd/cmd/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -36,7 +38,7 @@ func runDoctor(cmd *cobra.Command, args []string) error {
{"Configuration Files", checkConfigFiles},
{"Environment Variables", checkEnvVars},
{"Port Availability (8080)", checkPortAvailable("8080")},
{"Port Availability (9080)", checkPortAvailable("9080")},
{"Admin API Health (9080)", checkAdminAPIHealth},
{"Database (SQLite)", checkDatabase},
}

Expand Down Expand Up @@ -147,3 +149,42 @@ func checkDatabase() (bool, string) {

return true, "database accessible"
}

// checkAdminAPIHealth verifies the Admin API server is responding.
// Unlike checkPortAvailable which only tests port reachability, this makes
// an actual HTTP request to confirm the Admin API is operational.
func checkAdminAPIHealth() (bool, string) {
adminPort := os.Getenv("HOTPLEX_ADMIN_PORT")
if adminPort == "" {
adminPort = "9080"
}

url := "http://localhost:" + adminPort + "/admin/v1/health"
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, "failed to create request"
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
// Provide actionable diagnostics based on error type
if strings.Contains(err.Error(), "connection refused") {
return false, "connection refused — is the daemon running? (hotplexd start)"
}
if strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "timeout") {
return false, "timeout — daemon may still be starting, try again shortly"
}
return false, "request failed: " + err.Error()
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
return false, fmt.Sprintf("unexpected status %d: %s", resp.StatusCode, string(body))
}

return true, "Admin API is healthy"
}
Loading