@@ -10,6 +10,7 @@ import (
1010 "os/exec"
1111 "runtime"
1212 "sort"
13+ "strings"
1314 "time"
1415
1516 "github.com/energye/systray" // needed for MenuItem type
@@ -407,16 +408,23 @@ func (app *App) rebuildMenu(ctx context.Context) {
407408 // Clear all existing menu items
408409 app .systrayInterface .ResetMenu ()
409410
410- // Check for auth error first
411- if app .authError != "" {
411+ // Check for errors (auth or connection failures)
412+ app .mu .RLock ()
413+ authError := app .authError
414+ failureCount := app .consecutiveFailures
415+ lastFetchError := app .lastFetchError
416+ app .mu .RUnlock ()
417+
418+ // Show auth error if present
419+ if authError != "" {
412420 // Show authentication error message
413421 errorTitle := app .systrayInterface .AddMenuItem ("⚠️ Authentication Error" , "" )
414422 errorTitle .Disable ()
415423
416424 app .systrayInterface .AddSeparator ()
417425
418426 // Add error details
419- errorMsg := app .systrayInterface .AddMenuItem (app . authError , "Click to see setup instructions" )
427+ errorMsg := app .systrayInterface .AddMenuItem (authError , "Click to see setup instructions" )
420428 errorMsg .Click (func () {
421429 if err := openURL (ctx , "https://cli.github.com/manual/gh_auth_login" ); err != nil {
422430 slog .Error ("failed to open setup instructions" , "error" , err )
@@ -449,6 +457,78 @@ func (app *App) rebuildMenu(ctx context.Context) {
449457 return
450458 }
451459
460+ // Show connection error if we have consecutive failures
461+ if failureCount > 0 && lastFetchError != "" {
462+ var errorMsg string
463+ switch {
464+ case failureCount == 1 :
465+ errorMsg = "⚠️ Connection Error"
466+ case failureCount <= 3 :
467+ errorMsg = fmt .Sprintf ("⚠️ Connection Issues (%d failures)" , failureCount )
468+ case failureCount <= 10 :
469+ errorMsg = "❌ Multiple Connection Failures"
470+ default :
471+ errorMsg = "💀 Service Degraded"
472+ }
473+
474+ errorTitle := app .systrayInterface .AddMenuItem (errorMsg , "" )
475+ errorTitle .Disable ()
476+
477+ // Determine hostname and error type
478+ hostname := "api.github.com"
479+ for _ , h := range []struct { match , host string }{
480+ {"ready-to-review.dev" , "dash.ready-to-review.dev" },
481+ {"api.github.com" , "api.github.com" },
482+ {"github.com" , "github.com" },
483+ } {
484+ if strings .Contains (lastFetchError , h .match ) {
485+ hostname = h .host
486+ break
487+ }
488+ }
489+
490+ errorType := "Connection failed"
491+ for _ , e := range []struct { match , errType string }{
492+ {"timeout" , "Request timeout" },
493+ {"context deadline" , "Request timeout (context deadline)" },
494+ {"rate limit" , "Rate limit exceeded" },
495+ {"401" , "Authentication failed" },
496+ {"unauthorized" , "Authentication failed" },
497+ {"403" , "Access forbidden" },
498+ {"forbidden" , "Access forbidden" },
499+ {"404" , "Resource not found" },
500+ {"connection refused" , "Connection refused" },
501+ {"no such host" , "DNS resolution failed" },
502+ {"TLS" , "TLS/Certificate error" },
503+ {"x509" , "TLS/Certificate error" },
504+ } {
505+ if strings .Contains (lastFetchError , e .match ) {
506+ errorType = e .errType
507+ break
508+ }
509+ }
510+
511+ // Show technical details
512+ techDetails := app .systrayInterface .AddMenuItem (fmt .Sprintf ("Host: %s" , hostname ), "" )
513+ techDetails .Disable ()
514+
515+ errorTypeItem := app .systrayInterface .AddMenuItem (fmt .Sprintf ("Error: %s" , errorType ), "" )
516+ errorTypeItem .Disable ()
517+
518+ // Show truncated raw error for debugging (max 80 chars)
519+ rawError := lastFetchError
520+ if len (rawError ) > 80 {
521+ rawError = rawError [:77 ] + "..."
522+ }
523+ rawErrorItem := app .systrayInterface .AddMenuItem (fmt .Sprintf ("Details: %s" , rawError ), "Click to copy full error" )
524+ rawErrorItem .Click (func () {
525+ // Would need clipboard support to implement copy
526+ slog .Info ("Full error" , "error" , lastFetchError )
527+ })
528+
529+ app .systrayInterface .AddSeparator ()
530+ }
531+
452532 // Update tray title
453533 app .setTrayTitle ()
454534
0 commit comments