@@ -13,7 +13,7 @@ import (
1313
1414// BuildBlocks creates Slack Block Kit UI for the home dashboard.
1515// Design matches dashboard at https://ready-to-review.dev - modern minimal with indigo accents.
16- func BuildBlocks (dashboard * Dashboard ) []slack.Block {
16+ func BuildBlocks (dashboard * Dashboard , userTZ string ) []slack.Block {
1717 var blocks []slack.Block
1818
1919 // Header
@@ -30,51 +30,53 @@ func BuildBlocks(dashboard *Dashboard) []slack.Block {
3030 slack .NewTextBlockObject ("plain_text" , "🔄 Refresh Dashboard" , true , false ),
3131 ).WithStyle ("primary" ),
3232 ),
33- slack .NewDividerBlock (),
3433 )
3534
36- // PR sections
37- blocks = append (blocks , BuildPRSections (dashboard .IncomingPRs , dashboard .OutgoingPRs )... )
38-
39- // Organizations section
40- blocks = append (blocks , slack .NewDividerBlock ())
41-
42- var orgLines []string
43- for _ , org := range dashboard .WorkspaceOrgs {
44- // URL-escape org name to prevent injection
45- esc := url .PathEscape (org )
46- orgLine := fmt .Sprintf ("• <%s|%s> [<%s|config>, <%s|dashboard>]" ,
47- fmt .Sprintf ("https://github.com/%s" , esc ),
48- org ,
49- fmt .Sprintf ("https://github.com/%s/.github/blob/main/.codeGROOVE/slack.yaml" , esc ),
50- fmt .Sprintf ("https://%s.ready-to-review.dev" , esc ),
51- )
52- orgLines = append (orgLines , orgLine )
53- }
54-
35+ // Updated timestamp - right after refresh button
36+ now := formatTimestamp (time .Now (), userTZ )
5537 blocks = append (blocks ,
56- slack .NewSectionBlock (
38+ slack .NewContextBlock ( "" ,
5739 slack .NewTextBlockObject ("mrkdwn" ,
58- "*Organizations* \n " + strings . Join ( orgLines , " \n " ),
40+ fmt . Sprintf ( "Updated %s" , now ),
5941 false ,
6042 false ,
6143 ),
62- nil ,
63- nil ,
6444 ),
45+ slack .NewDividerBlock (),
6546 )
6647
67- // Updated timestamp
68- now := time .Now ().Format ("Jan 2, 3:04pm MST" )
69- blocks = append (blocks ,
70- slack .NewContextBlock ("" ,
71- slack .NewTextBlockObject ("mrkdwn" ,
72- fmt .Sprintf ("Updated: %s" , now ),
73- false ,
74- false ,
48+ // PR sections
49+ blocks = append (blocks , BuildPRSections (dashboard .IncomingPRs , dashboard .OutgoingPRs )... )
50+
51+ // Organizations section - only show if there are orgs configured
52+ if len (dashboard .WorkspaceOrgs ) > 0 {
53+ blocks = append (blocks , slack .NewDividerBlock ())
54+
55+ var orgLines []string
56+ for _ , org := range dashboard .WorkspaceOrgs {
57+ // URL-escape org name to prevent injection
58+ esc := url .PathEscape (org )
59+ orgLine := fmt .Sprintf ("• <%s|%s> [<%s|config>, <%s|dashboard>]" ,
60+ fmt .Sprintf ("https://github.com/%s" , esc ),
61+ org ,
62+ fmt .Sprintf ("https://github.com/%s/.github/blob/main/.codeGROOVE/slack.yaml" , esc ),
63+ fmt .Sprintf ("https://%s.ready-to-review.dev" , esc ),
64+ )
65+ orgLines = append (orgLines , orgLine )
66+ }
67+
68+ blocks = append (blocks ,
69+ slack .NewSectionBlock (
70+ slack .NewTextBlockObject ("mrkdwn" ,
71+ "*Organizations*\n " + strings .Join (orgLines , "\n " ),
72+ false ,
73+ false ,
74+ ),
75+ nil ,
76+ nil ,
7577 ),
76- ),
77- )
78+ )
79+ }
7880
7981 return blocks
8082}
@@ -223,12 +225,13 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
223225}
224226
225227// BuildBlocksWithDebug creates Slack Block Kit UI with debug information about user mapping.
226- func BuildBlocksWithDebug (dashboard * Dashboard , mapping * usermapping.ReverseMapping ) []slack.Block {
228+ func BuildBlocksWithDebug (dashboard * Dashboard , userTZ string , mapping * usermapping.ReverseMapping ) []slack.Block {
227229 // Build standard blocks first
228- blocks := BuildBlocks (dashboard )
230+ blocks := BuildBlocks (dashboard , userTZ )
229231
230- // Add debug section if mapping info is available
231- if mapping != nil {
232+ // Add debug section based on mapping status
233+ switch {
234+ case mapping != nil :
232235 blocks = append (blocks ,
233236 slack .NewDividerBlock (),
234237 slack .NewSectionBlock (
@@ -260,8 +263,27 @@ func BuildBlocksWithDebug(dashboard *Dashboard, mapping *usermapping.ReverseMapp
260263 ),
261264 )
262265 }
263- } else {
264- // No mapping found - show error message
266+ case len (dashboard .WorkspaceOrgs ) == 0 :
267+ // No organizations configured for this workspace (likely startup/race condition)
268+ blocks = append (blocks ,
269+ slack .NewDividerBlock (),
270+ slack .NewSectionBlock (
271+ slack .NewTextBlockObject ("mrkdwn" ,
272+ "⏳ *Setting up workspace...*\n " +
273+ "No organizations configured yet. This usually resolves automatically.\n \n " +
274+ "If this persists:\n " +
275+ "1. Ensure your GitHub App is installed for your organization\n " +
276+ "2. Check that `.codeGROOVE/slack.yaml` exists in your org's `.github` repo\n " +
277+ "3. Verify the `slack:` field matches this workspace's domain" ,
278+ false ,
279+ false ,
280+ ),
281+ nil ,
282+ nil ,
283+ ),
284+ )
285+ default :
286+ // User mapping failed
265287 blocks = append (blocks ,
266288 slack .NewDividerBlock (),
267289 slack .NewSectionBlock (
@@ -279,3 +301,20 @@ func BuildBlocksWithDebug(dashboard *Dashboard, mapping *usermapping.ReverseMapp
279301
280302 return blocks
281303}
304+
305+ // formatTimestamp formats a timestamp in the user's timezone without the colon after "Updated".
306+ // Example: "Nov 6, 12:48am America/Los_Angeles".
307+ func formatTimestamp (t time.Time , tzName string ) string {
308+ // Load the timezone
309+ loc , err := time .LoadLocation (tzName )
310+ if err != nil {
311+ // Fallback to UTC if timezone is invalid
312+ loc = time .UTC
313+ }
314+
315+ // Convert to user's timezone
316+ t = t .In (loc )
317+
318+ // Format as "Jan 2, 3:04pm MST"
319+ return t .Format ("Jan 2, 3:04pm MST" )
320+ }
0 commit comments