@@ -2,16 +2,19 @@ package daemon
22
33import (
44 "fmt"
5+ "image/color"
56 "log"
67
78 "fyne.io/fyne/v2"
9+ "fyne.io/fyne/v2/canvas"
810 "fyne.io/fyne/v2/container"
911 "fyne.io/fyne/v2/dialog"
1012 "fyne.io/fyne/v2/layout"
1113 "fyne.io/fyne/v2/widget"
1214
1315 "github.com/linuskendall/cosmonaut/internal/codespace"
1416 "github.com/linuskendall/cosmonaut/internal/config"
17+ "github.com/linuskendall/cosmonaut/internal/doctor"
1518)
1619
1720// buildSettingsPanel builds the settings content panel for the unified window.
@@ -22,6 +25,12 @@ func (d *Daemon) buildSettingsPanel(win fyne.Window) fyne.CanvasObject {
2225 heading .TextStyle = fyne.TextStyle {Bold : true }
2326 items = append (items , heading )
2427
28+ // Health checks: doctor catalog with per-check status and fix
29+ // buttons. Mirrors what the main-window banner shows, but stays
30+ // visible even if the user dismissed banners earlier.
31+ items = append (items , d .buildHealthSection (win ))
32+ items = append (items , widget .NewSeparator ())
33+
2534 // GitHub auth section.
2635 items = append (items , d .buildAuthSection (win ))
2736 items = append (items , widget .NewSeparator ())
@@ -71,6 +80,125 @@ func (d *Daemon) showPreferences() {
7180 })
7281}
7382
83+ // buildHealthSection lists every doctor check with its current status
84+ // and a Fix button when applicable. Even if a user dismissed the main
85+ // window banner, the same fix is reachable here.
86+ func (d * Daemon ) buildHealthSection (win fyne.Window ) fyne.CanvasObject {
87+ heading := widget .NewLabel ("Health checks" )
88+ heading .TextStyle = fyne.TextStyle {Bold : true }
89+
90+ rebuild := func () {
91+ if win != nil {
92+ win .SetContent (d .buildSettingsPanel (win ))
93+ }
94+ // Also refresh the main window banner if it's open.
95+ d .refreshMainWindowBanner ()
96+ }
97+
98+ rows := []fyne.CanvasObject {heading }
99+ for _ , c := range doctor .Catalog (d .ListErr ) {
100+ rows = append (rows , d .buildHealthRow (c , win , rebuild ))
101+ }
102+ return container .NewVBox (rows ... )
103+ }
104+
105+ func (d * Daemon ) buildHealthRow (c doctor.Check , win fyne.Window , rebuild func ()) fyne.CanvasObject {
106+ issue := c .Status ()
107+
108+ var dotColor color.Color
109+ var statusText string
110+ switch {
111+ case issue == nil :
112+ dotColor = cLime
113+ statusText = "OK"
114+ case issue .Severity == doctor .SeverityError :
115+ dotColor = cRed
116+ statusText = "Error"
117+ default :
118+ dotColor = cOrange
119+ statusText = "Warning"
120+ }
121+ dot := canvas .NewCircle (dotColor )
122+ dot .StrokeWidth = 0
123+ dot .Resize (fyne .NewSize (8 , 8 ))
124+
125+ title := widget .NewLabel (c .Title )
126+ title .TextStyle = fyne.TextStyle {Bold : true }
127+
128+ status := canvas .NewText (statusText , dotColor )
129+ status .TextSize = 11
130+ status .TextStyle = fyne.TextStyle {Monospace : true , Bold : true }
131+
132+ header := container .NewHBox (container .NewCenter (dot ), title , layout .NewSpacer (), status )
133+
134+ var detail fyne.CanvasObject
135+ if issue != nil {
136+ lbl := widget .NewLabel (issue .Summary )
137+ lbl .Wrapping = fyne .TextWrapWord
138+ detail = lbl
139+ } else {
140+ // When passing, show the description so the user understands
141+ // what was checked.
142+ lbl := widget .NewLabel (c .Description )
143+ lbl .Wrapping = fyne .TextWrapWord
144+ detail = lbl
145+ }
146+
147+ var actions fyne.CanvasObject
148+ if issue != nil {
149+ var btn * widget.Button
150+ switch {
151+ case c .HasInProcessFix ():
152+ btn = primaryButton ("Fix" , func () {
153+ go func () {
154+ if err := c .Fix (); err != nil {
155+ fyne .Do (func () {
156+ dialog .ShowError (fmt .Errorf ("fix %s: %w" , c .ID , err ), win )
157+ })
158+ return
159+ }
160+ fyne .Do (rebuild )
161+ }()
162+ })
163+ case c .HasTerminalFix ():
164+ btn = primaryButton ("Fix in terminal" , func () {
165+ cmd := c .FixCommand () + `; echo; echo "Press enter to close"; read _`
166+ go openCommandInTerminal (cmd )
167+ })
168+ }
169+ recheckBtn := widget .NewButton ("Re-check" , func () { rebuild () })
170+ row := container .NewHBox (layout .NewSpacer ())
171+ if btn != nil {
172+ row .Add (btn )
173+ }
174+ row .Add (recheckBtn )
175+ // If the user previously dismissed the banner, surface a way
176+ // to bring it back.
177+ if d .IsDismissed (c .ID ) {
178+ restoreBtn := widget .NewButton ("Show banner again" , func () {
179+ d .UndismissCheck (c .ID )
180+ rebuild ()
181+ })
182+ row .Add (restoreBtn )
183+ }
184+ actions = row
185+ }
186+
187+ if actions == nil {
188+ return container .NewPadded (container .NewVBox (header , detail ))
189+ }
190+ return container .NewPadded (container .NewVBox (header , detail , actions ))
191+ }
192+
193+ // refreshMainWindowBanner re-renders the top banner of an open main
194+ // window if there is one, so a fix applied (or banner restored) from
195+ // the Settings page is reflected immediately.
196+ func (d * Daemon ) refreshMainWindowBanner () {
197+ if uw := d .activeUnifiedWindow (); uw != nil {
198+ uw .refreshBanner ()
199+ }
200+ }
201+
74202func (d * Daemon ) buildAuthSection (win fyne.Window ) fyne.CanvasObject {
75203 authed := codespace .EnsureGHAuth (d .Runner ) == nil
76204
@@ -99,7 +227,7 @@ func (d *Daemon) buildAuthSection(win fyne.Window) fyne.CanvasObject {
99227 actionBtn = widget .NewButton ("Remove auth" , func () {
100228 actionBtn .Disable ()
101229 go func () {
102- _ , err := d .Runner .Run ([]string {"auth" , "logout" , "--hostname" , "github.com" , "--yes" })
230+ _ , err := d .Runner .Run ([]string {"auth" , "logout" , "--hostname" , "github.com" })
103231 fyne .Do (func () {
104232 if err != nil {
105233 log .Printf ("auth logout: %v" , err )
0 commit comments