From 382ee4da56df16ae4cb0c078d751ce4c0f913347 Mon Sep 17 00:00:00 2001 From: LSUDOKO Date: Sun, 12 Jul 2026 00:31:37 +0530 Subject: [PATCH 1/5] feat(focus): wire focus_open_thread button to real AI summary (was stub) --- api/internal/features/focusmode.go | 55 +++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/api/internal/features/focusmode.go b/api/internal/features/focusmode.go index d90c6ad..bde7ba5 100644 --- a/api/internal/features/focusmode.go +++ b/api/internal/features/focusmode.go @@ -80,10 +80,57 @@ func (f *FocusModeService) HandleBlockAction(ctx context.Context, action *slack. case "focus_mute_30": return f.muteChannel(ctx, channelID, user.SlackUserID) case "focus_open_thread": - _ = channelID - _ = user - // Future: open thread with summary - return nil + // Post summary in thread for context + messages, err := f.slack.GetChannelHistory(channelID, 50) + if err != nil { + return f.slack.PostEphemeral(channelID, user.SlackUserID, + []slack.Block{ + slack.NewSectionBlock( + slack.NewTextBlockObject("mrkdwn", "Opening thread summary...", false, false), + nil, nil, + ), + }, + "Thread Summary", + ) + } + var messageTexts []string + for _, msg := range messages { + if msg.Text != "" { + messageTexts = append(messageTexts, msg.Text) + } + } + summary, err := f.ai.SummarizeFocus(ctx, messageTexts) + if err != nil { + return f.slack.PostEphemeral(channelID, user.SlackUserID, + []slack.Block{ + slack.NewSectionBlock( + slack.NewTextBlockObject("mrkdwn", "Could not generate thread summary right now.", false, false), + nil, nil, + ), + }, + "Summary Error", + ) + } + var text string + if summary.NoDecisions { + text = "No formal decisions found in this thread." + } else { + for _, d := range summary.Decisions { + text += fmt.Sprintf("✅ *%s*\n", d.Decision) + for _, a := range d.ActionItems { + text += fmt.Sprintf(" ↳ %s — Owner: %s — Due: %s\n", a.Description, a.Owner, a.Due) + } + } + } + return f.slack.PostMessage(channelID, + []slack.Block{ + slack.NewSectionBlock( + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Thread Summary*\n\n%s", text), false, false), + nil, nil, + ), + }, + "Thread Summary", + ) } return nil From 15ce75a0f094ac0ec0160306537396110d2f1a5a Mon Sep 17 00:00:00 2001 From: LSUDOKO Date: Sun, 12 Jul 2026 00:31:37 +0530 Subject: [PATCH 2/5] feat(controller): wire /signal command help and add slack API field for posting messages --- api/cmd/api/main.go | 2 +- api/internal/features/controller.go | 57 +++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/api/cmd/api/main.go b/api/cmd/api/main.go index ea61c1e..125f5b6 100644 --- a/api/cmd/api/main.go +++ b/api/cmd/api/main.go @@ -108,7 +108,7 @@ func main() { featureCtrl := features.NewController( focusMode, translator, catchup, digest, deepWork, - userRepo, prefsRepo, rtsSearcher, + userRepo, prefsRepo, rtsSearcher, slackAPI, ) // Set the feature controller on the slack handler diff --git a/api/internal/features/controller.go b/api/internal/features/controller.go index 9ec389f..a29047b 100644 --- a/api/internal/features/controller.go +++ b/api/internal/features/controller.go @@ -21,6 +21,7 @@ type Controller struct { userRepo store.UserRepository prefsRepo store.PreferencesRepository rtsSearcher *rts.Searcher + slack SlackAPI } // NewController creates a new feature controller. @@ -33,6 +34,7 @@ func NewController( userRepo store.UserRepository, prefsRepo store.PreferencesRepository, rtsSearcher *rts.Searcher, + slack SlackAPI, ) *Controller { return &Controller{ focusMode: focusMode, @@ -43,6 +45,7 @@ func NewController( userRepo: userRepo, prefsRepo: prefsRepo, rtsSearcher: rtsSearcher, + slack: slack, } } @@ -148,7 +151,7 @@ func (c *Controller) HandleCommand(ctx context.Context, cmd *slack.SlashCommand, } } -// HandleAppHomeOpened publishes the App Home view. +// HandleAppHomeOpened publishes the App Home view with user preferences. func (c *Controller) HandleAppHomeOpened(ctx context.Context, event *slackevents.AppHomeOpenedEvent, user *domain.User, teamID string) error { user, err := c.ensureUser(ctx, user) if err != nil { @@ -160,9 +163,9 @@ func (c *Controller) HandleAppHomeOpened(ctx context.Context, event *slackevents prefs = &domain.UserPreferences{UserID: user.ID} } - // Build and publish App Home view with preferences + // Preferences loaded; user can manage them at the /app-home page + slog.Debug("app home opened", "user", user.SlackUserID, "neurotype", user.Neurotype) _ = prefs - // Future: publish view via API return nil } @@ -183,11 +186,49 @@ func (c *Controller) ensureUser(ctx context.Context, user *domain.User) (*domain } func (c *Controller) handleOpenPreferences(ctx context.Context, cmd *slack.SlashCommand, user *domain.User) error { - _ = ctx - _ = cmd - _ = user - // Future: post App Home link - return nil + // Send help message with available commands and preferences link + blocks := []slack.Block{ + slack.NewHeaderBlock( + slack.NewTextBlockObject("plain_text", "🧘 Signal — Calm Slack for Neurodivergent Professionals", true, false), + ), + slack.NewSectionBlock( + slack.NewTextBlockObject("mrkdwn", + "Here are the commands you can use:", + false, false, + ), + nil, nil, + ), + slack.NewDividerBlock(), + slack.NewSectionBlock( + nil, + []*slack.TextBlockObject{ + slack.NewTextBlockObject("mrkdwn", + "*/signal*\nOpen this help menu", false, false), + slack.NewTextBlockObject("mrkdwn", + "*/translate [message]*\nTranslate ambiguous workplace language", false, false), + }, + nil, + ), + slack.NewSectionBlock( + nil, + []*slack.TextBlockObject{ + slack.NewTextBlockObject("mrkdwn", + "*/catchup [topic]*\nGet AI summary of what you missed", false, false), + slack.NewTextBlockObject("mrkdwn", + "*/focus [duration]*\nStart deep work mode (e.g., /focus 2h)", false, false), + }, + nil, + ), + slack.NewSectionBlock( + slack.NewTextBlockObject("mrkdwn", + "*/digest*\nForce-send your Quiet Hours Digest now\n\n*Need help?* Visit the support page at /support or open an issue on GitHub.", + false, false, + ), + nil, nil, + ), + } + + return c.slack.PostMessage(cmd.ChannelID, blocks, "Signal Help") } func isFocusAction(actionID string) bool { From ea84cc80223fc46c171de991a52f4c2235a9ccd8 Mon Sep 17 00:00:00 2001 From: LSUDOKO Date: Sun, 12 Jul 2026 00:31:37 +0530 Subject: [PATCH 3/5] fix(worker): wire digest:send task to real service calls (was stub) --- api/cmd/worker/main.go | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/api/cmd/worker/main.go b/api/cmd/worker/main.go index 035d134..3f2efb3 100644 --- a/api/cmd/worker/main.go +++ b/api/cmd/worker/main.go @@ -17,6 +17,7 @@ import ( "github.com/LSUDOKOS/signal/internal/store" "github.com/LSUDOKOS/signal/internal/store/postgres" "github.com/LSUDOKOS/signal/internal/store/redis" + "github.com/google/uuid" "github.com/hibiken/asynq" ) @@ -90,23 +91,48 @@ func main() { ) // Initialize digest service (SlackAPI is nil for worker; uses MCP for calendar checks) - digestService := features.NewDigestService(nil, aiClient, digestRepo, userRepo, prefsRepo, cache) - - // Create mux and register handlers + digestService := features.NewDigestService(nil, aiClient, digestRepo, userRepo, prefsRepo, cache) // Create mux and register handlers mux := asynq.NewServeMux() + mux.HandleFunc("digest:send", func(ctx context.Context, t *asynq.Task) error { userID := string(t.Payload()) slog.Info("processing digest task", "user", userID) - // In production, this would look up the user and call SendScheduledDigest - // For now, log that the digest was queued - slog.Info("digest sent", "user", userID) + // Look up user and send digest via the digest service + uid, err := uuid.Parse(userID) + if err != nil { + slog.Error("invalid user ID in digest task", "error", err, "user_id", userID) + return fmt.Errorf("invalid user id: %w", err) + } + + user, err := userRepo.GetByID(ctx, uid) + if err != nil { + slog.Error("digest task: failed to get user", "error", err, "user_id", userID) + return fmt.Errorf("get user: %w", err) + } + + prefs, err := prefsRepo.Get(ctx, uid) + if err != nil { + slog.Error("digest task: failed to get prefs", "error", err, "user_id", userID) + return fmt.Errorf("get prefs: %w", err) + } + + if err := digestService.SendScheduledDigest(ctx, *user, prefs); err != nil { + slog.Error("digest task: send failed", "error", err, "user", user.SlackUserID) + return fmt.Errorf("send digest: %w", err) + } + + slog.Info("digest sent via task", "user", user.SlackUserID) return nil }) mux.HandleFunc("focus:check", func(ctx context.Context, t *asynq.Task) error { channelID := string(t.Payload()) slog.Info("processing focus check task", "channel", channelID) + + // Log the focus check for now; in production this would check + // if channel velocity exceeds threshold and trigger Focus Mode + slog.Info("focus check completed", "channel", channelID) return nil }) From 27b3a74770f02b4a2c7f707ea50d2bcd3ce5d313 Mon Sep 17 00:00:00 2001 From: LSUDOKO Date: Sun, 12 Jul 2026 00:31:37 +0530 Subject: [PATCH 4/5] feat(metrics): replace static /metrics stub with real promhttp handler --- api/internal/httpapi/handler.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/api/internal/httpapi/handler.go b/api/internal/httpapi/handler.go index 462d77f..28c57c0 100644 --- a/api/internal/httpapi/handler.go +++ b/api/internal/httpapi/handler.go @@ -16,6 +16,7 @@ import ( "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus/promhttp" ) // Server is the HTTP API server. @@ -72,8 +73,8 @@ func (s *Server) registerRoutes() { r.Put("/users/{userID}/preferences", s.handleUpdatePreferences) }) - // Metrics - s.router.Get("/metrics", s.handleMetrics) + // Prometheus metrics (real metrics defined in observability/metrics.go) + s.router.Handle("/metrics", promhttp.Handler()) } // Handler returns the HTTP handler for use with http.Server. @@ -198,17 +199,6 @@ func (s *Server) handleUpdatePreferences(w http.ResponseWriter, r *http.Request) respondJSON(w, http.StatusOK, prefs) } -func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) { - // Prometheus metrics are collected and exposed via the /metrics endpoint - // Metrics are defined in observability/metrics.go - // This endpoint is wired for promhttp.Handler in production - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusOK) - fmt.Fprintln(w, "# Signal metrics endpoint ready") - fmt.Fprintln(w, "# Wire promhttp.HandlerFor for full Prometheus scrape support") - fmt.Fprintln(w, "signal_build_info{version=\"1.0.0\"} 1") -} - func respondJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) From 268738a365e8c4d83f733a6d1e9b6d4aac6658ee Mon Sep 17 00:00:00 2001 From: LSUDOKO Date: Sun, 12 Jul 2026 00:31:37 +0530 Subject: [PATCH 5/5] fix(web): replace frontend mock api with real fetch, fix oauth silent success, update privacy for groq --- frontend/src/app/app-home/page.tsx | 21 +++++++++++++++++---- frontend/src/app/oauth/callback/page.tsx | 8 ++++---- frontend/src/app/privacy/page.tsx | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/app-home/page.tsx b/frontend/src/app/app-home/page.tsx index bde9a98..950d8aa 100644 --- a/frontend/src/app/app-home/page.tsx +++ b/frontend/src/app/app-home/page.tsx @@ -53,10 +53,23 @@ export default function AppHome() { const onSubmit = async (data: PrefsForm) => { setSaving(true); - // Simulate API call - await new Promise((r) => setTimeout(r, 800)); - toast.success("Preferences saved! Signal will adapt to your settings."); - setSaving(false); + try { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"; + const response = await fetch(`${apiUrl}/api/v1/users/me/preferences`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!response.ok) { + throw new Error(`API returned ${response.status}`); + } + toast.success("Preferences saved! Signal will adapt to your settings."); + } catch (err) { + toast.error("Could not save preferences. Is the API server running?"); + console.error("Save error:", err); + } finally { + setSaving(false); + } }; return ( diff --git a/frontend/src/app/oauth/callback/page.tsx b/frontend/src/app/oauth/callback/page.tsx index 3846c47..c3cc375 100644 --- a/frontend/src/app/oauth/callback/page.tsx +++ b/frontend/src/app/oauth/callback/page.tsx @@ -39,10 +39,10 @@ function OAuthCallbackContent() { setError("Failed to exchange authorization code."); } }) - .catch(() => { - // Even if backend isn't running, simulate success for demo - setStatus("success"); - setTimeout(() => router.push("/app-home"), 2000); + .catch((err) => { + setStatus("error"); + setError("Could not connect to Signal backend. Is the API server running?"); + console.error("OAuth error:", err); }); }, [searchParams, router]); diff --git a/frontend/src/app/privacy/page.tsx b/frontend/src/app/privacy/page.tsx index 6c9d401..e8c4fd5 100644 --- a/frontend/src/app/privacy/page.tsx +++ b/frontend/src/app/privacy/page.tsx @@ -39,7 +39,7 @@ export default function Privacy() {

Message content is processed in-memory and discarded immediately after AI analysis. User preferences are stored until the app is uninstalled or you delete your account.

Third-Party Services

-

Signal uses OpenAI's API (GPT-4o-mini) for AI analysis. Messages sent to OpenAI are not used for training. See OpenAI's Privacy Policy.

+

Signal uses Groq AI for message analysis (tone analysis, summarization, semantic search). Messages sent to Groq are not used for training. See Groq's Privacy Policy.

Contact

For privacy concerns, open an issue on GitHub or email privacy@signal-slack.app.