Skip to content
Merged
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
148 changes: 20 additions & 128 deletions pkg/dailyreport/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"context"
"fmt"
"log/slog"
"sort"
"strings"
"time"

"github.com/codeGROOVE-dev/slacker/pkg/home"
Expand Down Expand Up @@ -224,146 +222,40 @@ func randomGreeting() string {
}

// Pick greeting based on time for variety
greetingIdx := (now.Hour()*60 + now.Minute()) % len(greetings)
return greetings[greetingIdx]
i := (now.Hour()*60 + now.Minute()) % len(greetings)
return greetings[i]
}

// formatPRLine formats a single PR as a line of text (goose-inspired format).
// Returns a string like: "■ repo#123 • title — action" or " repo#456 • title".
func formatPRLine(pr *home.PR) string {
// Extract repo name from "org/repo" format
parts := strings.SplitN(pr.Repository, "/", 2)
repo := pr.Repository
if len(parts) == 2 {
repo = parts[1]
}

// Determine bullet character based on blocking status
var bullet string
switch {
case pr.IsBlocked || pr.NeedsReview:
// Critical: blocked on user
bullet = "■"
case pr.ActionKind != "":
// Non-critical: has action but not blocking
bullet = "•"
default:
// No action for user - use 2-space indent to align with bullets
bullet = " "
}

// Build PR reference with link
ref := fmt.Sprintf("<%s|%s#%d>", pr.URL, repo, pr.Number)

// Build line: bullet repo#123 • title
line := fmt.Sprintf("%s %s • %s", bullet, ref, pr.Title)

// Add action kind if present (only show user's next action)
if pr.ActionKind != "" {
action := strings.ReplaceAll(pr.ActionKind, "_", " ")
line = fmt.Sprintf("%s — %s", line, action)
}

return line
}

// BuildReportBlocks creates Block Kit blocks for a daily report.
// Format inspired by goose - simple, minimal, action-focused.
// BuildReportBlocks creates Block Kit blocks for a daily report with greeting.
// Uses home.BuildPRSections for consistent formatting with the dashboard.
func BuildReportBlocks(incoming, outgoing []home.PR) []slack.Block {
// Sort PRs by most recently updated first (make copies to avoid modifying input)
incomingSorted := make([]home.PR, len(incoming))
copy(incomingSorted, incoming)
sort.Slice(incomingSorted, func(i, j int) bool {
return incomingSorted[i].UpdatedAt.After(incomingSorted[j].UpdatedAt)
})

outgoingSorted := make([]home.PR, len(outgoing))
copy(outgoingSorted, outgoing)
sort.Slice(outgoingSorted, func(i, j int) bool {
return outgoingSorted[i].UpdatedAt.After(outgoingSorted[j].UpdatedAt)
})
slog.Info("building report blocks",
"incoming_count", len(incoming),
"outgoing_count", len(outgoing))

// Log outgoing PRs to debug blocking detection
for i := range outgoing {
slog.Info("outgoing PR for report",
"pr", outgoing[i].URL,
"title", outgoing[i].Title,
"is_blocked", outgoing[i].IsBlocked,
"action_kind", outgoing[i].ActionKind,
"action_reason", outgoing[i].ActionReason)
}

var blocks []slack.Block

// Greeting
greeting := randomGreeting()
blocks = append(blocks,
slack.NewSectionBlock(
slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("%s Here is your daily report:", greeting), false, false),
slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("%s Here is your daily report:", randomGreeting()), false, false),
nil,
nil,
),
)

// Incoming PRs section (only if there are incoming PRs)
if len(incomingSorted) > 0 {
// Count blocked PRs
n := 0
for i := range incomingSorted {
if incomingSorted[i].IsBlocked || incomingSorted[i].NeedsReview {
n++
}
}

// Section header
header := "*Incoming*"
if n > 0 {
if n == 1 {
header = "*Incoming — 1 blocked on you*"
} else {
header = fmt.Sprintf("*Incoming — %d blocked on you*", n)
}
}

// Build PR list
var prLines []string
for i := range incomingSorted {
prLines = append(prLines, formatPRLine(&incomingSorted[i]))
}

blocks = append(blocks,
slack.NewSectionBlock(
slack.NewTextBlockObject("mrkdwn", header+"\n\n"+strings.Join(prLines, "\n"), false, false),
nil,
nil,
),
)
}

// Outgoing PRs section (only if there are outgoing PRs)
if len(outgoingSorted) > 0 {
// Count blocked PRs
n := 0
for i := range outgoingSorted {
if outgoingSorted[i].IsBlocked {
n++
}
}

// Section header
header := "*Outgoing*"
if n > 0 {
if n == 1 {
header = "*Outgoing — 1 blocked on you*"
} else {
header = fmt.Sprintf("*Outgoing — %d blocked on you*", n)
}
}

// Build PR list
var prLines []string
for i := range outgoingSorted {
prLines = append(prLines, formatPRLine(&outgoingSorted[i]))
}

blocks = append(blocks,
slack.NewSectionBlock(
slack.NewTextBlockObject("mrkdwn", header+"\n\n"+strings.Join(prLines, "\n"), false, false),
nil,
nil,
),
)
}
// Add PR sections (uses home.BuildPRSections for unified formatting)
blocks = append(blocks, home.BuildPRSections(incoming, outgoing)...)

return blocks
}
67 changes: 64 additions & 3 deletions pkg/home/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package home

import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
Expand Down Expand Up @@ -254,8 +255,15 @@ func (f *Fetcher) enrichPRs(ctx context.Context, prs []PR, githubUsername string

results := make(chan enrichResult, len(prs))

for i := range prs {
for prIdx := range prs {
go func(idx int, pr PR) {
slog.Info("calling turnclient for PR enrichment",
"pr", pr.URL,
"github_user", githubUsername,
"incoming", incoming,
"bot_username", f.botUsername,
"last_event_time", pr.LastEventTime)

// Use LastEventTime for cache optimization - it's the most recent timestamp
// we know about (max of GitHub UpdatedAt and sprinkler LastEventTime)
var checkResult *turn.CheckResponse
Expand All @@ -273,13 +281,42 @@ func (f *Fetcher) enrichPRs(ctx context.Context, prs []PR, githubUsername string
retry.Context(ctx),
)
if err != nil {
slog.Debug("turnclient check failed after retries, using basic PR data",
slog.Warn("turnclient check failed after retries, using basic PR data",
"pr", pr.URL,
"github_user", githubUsername,
"incoming", incoming,
"error", err)
results <- enrichResult{idx: idx, pr: pr}
return
}

// Marshal full response for debugging
responseJSON, err := json.Marshal(checkResult)
if err != nil {
slog.Warn("failed to marshal turnclient response", "pr", pr.URL, "error", err)
} else {
slog.Info("turnclient full response",
"pr", pr.URL,
"github_user", githubUsername,
"incoming", incoming,
"last_event_time_param", pr.LastEventTime,
"response_json", string(responseJSON))
}

slog.Info("turnclient returned response",
"pr", pr.URL,
"github_user", githubUsername,
"incoming", incoming,
"workflow_state", checkResult.Analysis.WorkflowState,
"next_action_count", len(checkResult.Analysis.NextAction),
"next_action_keys", func() []string {
keys := make([]string, 0, len(checkResult.Analysis.NextAction))
for k := range checkResult.Analysis.NextAction {
keys = append(keys, k)
}
return keys
}())

// Extract action for this user
if action, exists := checkResult.Analysis.NextAction[githubUsername]; exists {
pr.ActionReason = action.Reason
Expand All @@ -290,6 +327,30 @@ func (f *Fetcher) enrichPRs(ctx context.Context, prs []PR, githubUsername string
} else {
pr.IsBlocked = action.Critical
}

slog.Info("found action for user in turnclient response",
"pr", pr.URL,
"github_user", githubUsername,
"action_kind", action.Kind,
"critical", action.Critical,
"reason", action.Reason,
"incoming", incoming,
"is_blocked", pr.IsBlocked,
"needs_review", pr.NeedsReview)
} else {
// No action for this user in the response
slog.Debug("no action for user in turnclient response",
"pr", pr.URL,
"github_user", githubUsername,
"incoming", incoming,
"workflow_state", checkResult.Analysis.WorkflowState,
"next_action_keys", func() []string {
keys := make([]string, 0, len(checkResult.Analysis.NextAction))
for k := range checkResult.Analysis.NextAction {
keys = append(keys, k)
}
return keys
}())
}

// Extract test state from Analysis
Expand All @@ -306,7 +367,7 @@ func (f *Fetcher) enrichPRs(ctx context.Context, prs []PR, githubUsername string
}

results <- enrichResult{idx: idx, pr: pr}
}(i, prs[i])
}(prIdx, prs[prIdx])
}

// Collect results in original order
Expand Down
Loading
Loading