@@ -27,7 +27,7 @@ func BuildBlocks(dashboard *Dashboard, userTZ string) []slack.Block {
2727 slack .NewButtonBlockElement (
2828 "refresh_dashboard" ,
2929 "refresh" ,
30- slack .NewTextBlockObject ("plain_text" , "🔄 Refresh Dashboard " , true , false ),
30+ slack .NewTextBlockObject ("plain_text" , "🔄 Refresh" , true , false ),
3131 ).WithStyle ("primary" ),
3232 ),
3333 )
@@ -52,15 +52,19 @@ func BuildBlocks(dashboard *Dashboard, userTZ string) []slack.Block {
5252 if len (dashboard .WorkspaceOrgs ) > 0 {
5353 blocks = append (blocks , slack .NewDividerBlock ())
5454
55+ // Sort orgs alphabetically
56+ sortedOrgs := make ([]string , len (dashboard .WorkspaceOrgs ))
57+ copy (sortedOrgs , dashboard .WorkspaceOrgs )
58+ sort .Strings (sortedOrgs )
59+
5560 var orgLines []string
56- for _ , org := range dashboard . WorkspaceOrgs {
61+ for _ , org := range sortedOrgs {
5762 // URL-escape org name to prevent injection
5863 esc := url .PathEscape (org )
59- orgLine := fmt .Sprintf ("• <%s|%s> [<%s|config>, <%s|dashboard>]" ,
60- fmt .Sprintf ("https://github.com/%s" , esc ),
64+ orgLine := fmt .Sprintf ("• %s [<%s|dashboard> | <%s|config>]" ,
6165 org ,
62- fmt .Sprintf ("https://github.com/%s/.github/blob/main/.codeGROOVE/slack.yaml" , esc ),
6366 fmt .Sprintf ("https://%s.ready-to-review.dev" , esc ),
67+ fmt .Sprintf ("https://github.com/%s/.codeGROOVE/blob/main/slack.yaml" , esc ),
6468 )
6569 orgLines = append (orgLines , orgLine )
6670 }
@@ -81,6 +85,49 @@ func BuildBlocks(dashboard *Dashboard, userTZ string) []slack.Block {
8185 return blocks
8286}
8387
88+ // formatPRLine formats a single PR line with indicator, repo, number, title, and optional action.
89+ func formatPRLine (pr PR , isIncoming bool ) string {
90+ // Extract repo name
91+ repo := pr .Repository
92+ if idx := strings .LastIndex (repo , "/" ); idx >= 0 {
93+ repo = repo [idx + 1 :]
94+ }
95+
96+ // Determine indicator
97+ var indicator string
98+ if isIncoming {
99+ switch {
100+ case pr .NeedsReview , pr .IsBlocked :
101+ indicator = ":red_circle:"
102+ case pr .ActionKind != "" :
103+ indicator = ":speech_balloon:"
104+ default :
105+ indicator = ":white_small_square:"
106+ }
107+ } else {
108+ switch {
109+ case pr .NeedsReview , pr .IsBlocked :
110+ indicator = ":large_green_circle:"
111+ case pr .ActionKind != "" :
112+ indicator = ":speech_balloon:"
113+ default :
114+ indicator = ":white_small_square:"
115+ }
116+ }
117+
118+ // Build line
119+ line := fmt .Sprintf ("%s <%s|%s#%d> • %s" , indicator , pr .URL , repo , pr .Number , pr .Title )
120+ if pr .ActionKind != "" {
121+ action := strings .ReplaceAll (pr .ActionKind , "_" , " " )
122+ // Bold the action if this PR is blocked on the user
123+ if pr .IsBlocked || (isIncoming && pr .NeedsReview ) {
124+ action = "*" + action + "*"
125+ }
126+ line = fmt .Sprintf ("%s — %s" , line , action )
127+ }
128+ return line
129+ }
130+
84131// BuildPRSections creates Block Kit blocks for PR sections (incoming/outgoing).
85132// Format inspired by goose - simple, minimal, action-focused.
86133// This is the core formatting used by both daily reports and the dashboard.
@@ -108,32 +155,7 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
108155 if prs [i ].IsBlocked || prs [i ].NeedsReview {
109156 n ++
110157 }
111-
112- // Format PR line - extract repo name
113- repo := prs [i ].Repository
114- if idx := strings .LastIndex (repo , "/" ); idx >= 0 {
115- repo = repo [idx + 1 :]
116- }
117-
118- // Determine indicator
119- var indicator string
120- switch {
121- case prs [i ].NeedsReview :
122- indicator = ":red_circle:"
123- case prs [i ].IsBlocked :
124- indicator = ":red_circle:"
125- case prs [i ].ActionKind != "" :
126- indicator = ":speech_balloon:"
127- default :
128- indicator = ":white_small_square:"
129- }
130-
131- // Build line
132- line := fmt .Sprintf ("%s <%s|%s#%d> • %s" , indicator , prs [i ].URL , repo , prs [i ].Number , prs [i ].Title )
133- if prs [i ].ActionKind != "" {
134- line = fmt .Sprintf ("%s — %s" , line , strings .ReplaceAll (prs [i ].ActionKind , "_" , " " ))
135- }
136- lines = append (lines , line )
158+ lines = append (lines , formatPRLine (prs [i ], true ))
137159 }
138160
139161 // Build header
@@ -154,6 +176,15 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
154176 )
155177 }
156178
179+ // Spacer between Incoming and Outgoing sections
180+ if len (incoming ) > 0 && len (outgoing ) > 0 {
181+ blocks = append (blocks ,
182+ slack .NewContextBlock ("" ,
183+ slack .NewTextBlockObject ("plain_text" , " " , false , false ),
184+ ),
185+ )
186+ }
187+
157188 // Outgoing PRs section
158189 if len (outgoing ) > 0 {
159190 // Sort: blocked first, then by most recent within each group
@@ -175,32 +206,7 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
175206 if prs [i ].IsBlocked {
176207 n ++
177208 }
178-
179- // Format PR line - extract repo name
180- repo := prs [i ].Repository
181- if idx := strings .LastIndex (repo , "/" ); idx >= 0 {
182- repo = repo [idx + 1 :]
183- }
184-
185- // Determine indicator
186- var indicator string
187- switch {
188- case prs [i ].NeedsReview :
189- indicator = ":large_green_circle:"
190- case prs [i ].IsBlocked :
191- indicator = ":large_green_circle:"
192- case prs [i ].ActionKind != "" :
193- indicator = ":speech_balloon:"
194- default :
195- indicator = ":white_small_square:"
196- }
197-
198- // Build line
199- line := fmt .Sprintf ("%s <%s|%s#%d> • %s" , indicator , prs [i ].URL , repo , prs [i ].Number , prs [i ].Title )
200- if prs [i ].ActionKind != "" {
201- line = fmt .Sprintf ("%s — %s" , line , strings .ReplaceAll (prs [i ].ActionKind , "_" , " " ))
202- }
203- lines = append (lines , line )
209+ lines = append (lines , formatPRLine (prs [i ], false ))
204210 }
205211
206212 // Build header
@@ -224,46 +230,80 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
224230 return blocks
225231}
226232
227- // BuildBlocksWithDebug creates Slack Block Kit UI with debug information about user mapping .
233+ // BuildBlocksWithDebug creates Slack Block Kit UI with GitHub username integrated into the header .
228234func BuildBlocksWithDebug (dashboard * Dashboard , userTZ string , mapping * usermapping.ReverseMapping ) []slack.Block {
229- // Build standard blocks first
230- blocks := BuildBlocks (dashboard , userTZ )
235+ var blocks []slack.Block
236+
237+ // Header with GitHub username if available
238+ headerText := "🚀 Ready to Review"
239+ if mapping != nil {
240+ headerText = fmt .Sprintf ("🚀 Ready to Review — @%s" , mapping .GitHubUsername )
241+ }
242+
243+ blocks = append (blocks ,
244+ slack .NewHeaderBlock (
245+ slack .NewTextBlockObject ("plain_text" , headerText , true , false ),
246+ ),
247+ // Refresh button
248+ slack .NewActionBlock (
249+ "refresh_actions" ,
250+ slack .NewButtonBlockElement (
251+ "refresh_dashboard" ,
252+ "refresh" ,
253+ slack .NewTextBlockObject ("plain_text" , "🔄 Refresh" , true , false ),
254+ ).WithStyle ("primary" ),
255+ ),
256+ slack .NewDividerBlock (),
257+ )
258+
259+ // PR sections
260+ blocks = append (blocks , BuildPRSections (dashboard .IncomingPRs , dashboard .OutgoingPRs )... )
261+
262+ // Add spacing after PR sections if we have content
263+ if len (dashboard .IncomingPRs ) > 0 || len (dashboard .OutgoingPRs ) > 0 {
264+ blocks = append (blocks ,
265+ slack .NewContextBlock ("" ,
266+ slack .NewTextBlockObject ("plain_text" , " " , false , false ),
267+ ),
268+ )
269+ }
270+
271+ // Organizations section - only show if there are orgs configured
272+ if len (dashboard .WorkspaceOrgs ) > 0 {
273+ blocks = append (blocks , slack .NewDividerBlock ())
274+
275+ // Sort orgs alphabetically
276+ sortedOrgs := make ([]string , len (dashboard .WorkspaceOrgs ))
277+ copy (sortedOrgs , dashboard .WorkspaceOrgs )
278+ sort .Strings (sortedOrgs )
279+
280+ var orgLines []string
281+ for _ , org := range sortedOrgs {
282+ // URL-escape org name to prevent injection
283+ esc := url .PathEscape (org )
284+ orgLine := fmt .Sprintf ("• %s [<%s|dashboard> | <%s|config>]" ,
285+ org ,
286+ fmt .Sprintf ("https://%s.ready-to-review.dev" , esc ),
287+ fmt .Sprintf ("https://github.com/%s/.codeGROOVE/blob/main/slack.yaml" , esc ),
288+ )
289+ orgLines = append (orgLines , orgLine )
290+ }
231291
232- // Add debug section based on mapping status
233- switch {
234- case mapping != nil :
235292 blocks = append (blocks ,
236- slack .NewDividerBlock (),
237293 slack .NewSectionBlock (
238294 slack .NewTextBlockObject ("mrkdwn" ,
239- fmt .Sprintf ("🔍 *Debug Info*\n " +
240- "GitHub: `@%s` • Mapped via: `%s` • Confidence: `%d%%`" ,
241- mapping .GitHubUsername ,
242- mapping .MatchMethod ,
243- mapping .Confidence ),
295+ "*Organizations*\n " + strings .Join (orgLines , "\n " ),
244296 false ,
245297 false ,
246298 ),
247299 nil ,
248300 nil ,
249301 ),
250302 )
303+ }
251304
252- // Add mapping guidance if confidence is low
253- if mapping .Confidence < 80 {
254- blocks = append (blocks ,
255- slack .NewContextBlock ("" ,
256- slack .NewTextBlockObject ("mrkdwn" ,
257- fmt .Sprintf ("⚠️ Low confidence mapping. Add manual override to `slack.yaml`:\n ```yaml\n users:\n %s: %s\n ```" ,
258- mapping .GitHubUsername ,
259- mapping .SlackEmail ),
260- false ,
261- false ,
262- ),
263- ),
264- )
265- }
266- case len (dashboard .WorkspaceOrgs ) == 0 :
305+ // Add helpful messages for error cases
306+ if mapping == nil && len (dashboard .WorkspaceOrgs ) == 0 {
267307 // No organizations configured for this workspace (likely startup/race condition)
268308 blocks = append (blocks ,
269309 slack .NewDividerBlock (),
@@ -282,7 +322,7 @@ func BuildBlocksWithDebug(dashboard *Dashboard, userTZ string, mapping *usermapp
282322 nil ,
283323 ),
284324 )
285- default :
325+ } else if mapping == nil {
286326 // User mapping failed
287327 blocks = append (blocks ,
288328 slack .NewDividerBlock (),
@@ -299,6 +339,19 @@ func BuildBlocksWithDebug(dashboard *Dashboard, userTZ string, mapping *usermapp
299339 )
300340 }
301341
342+ // Updated timestamp at the bottom
343+ now := formatTimestamp (time .Now (), userTZ )
344+ blocks = append (blocks ,
345+ slack .NewDividerBlock (),
346+ slack .NewContextBlock ("" ,
347+ slack .NewTextBlockObject ("mrkdwn" ,
348+ fmt .Sprintf ("Updated %s" , now ),
349+ false ,
350+ false ,
351+ ),
352+ ),
353+ )
354+
302355 return blocks
303356}
304357
0 commit comments