@@ -26,11 +26,13 @@ type App struct {
2626 iopsTrack * datasource.DiskIOPSTracker
2727 syncTracker * datasource.SyncTracker
2828 alertMgr * datasource.AlertManager
29+ logTailer * datasource.LogTailer
2930 datadir string
3031}
3132
3233// New creates an App that reads from the given datadir.
3334func New (datadir string ) * App {
35+ logPath := filepath .Join (datadir , "logs" , "erigon.log" )
3436 return & App {
3537 datadir : datadir ,
3638 tview : tview .NewApplication (),
@@ -40,9 +42,17 @@ func New(datadir string) *App {
4042 iopsTrack : datasource .NewDiskIOPSTracker (),
4143 syncTracker : datasource .NewSyncTracker (),
4244 alertMgr : datasource .NewAlertManager (),
45+ logTailer : datasource .NewLogTailer (logPath ),
4346 }
4447}
4548
49+ // Page name constants.
50+ const (
51+ pageStart = "start"
52+ pageNodeInfo = "nodeInfo"
53+ pageLogs = "logs"
54+ )
55+
4656// Run starts the TUI event loop. It blocks until the user quits or the parent
4757// context is cancelled (e.g. by an OS signal).
4858func (a * App ) Run (parent context.Context , infoCh <- chan * commands.StagesInfo , errCh chan error ) error {
@@ -66,6 +76,25 @@ func (a *App) Run(parent context.Context, infoCh <-chan *commands.StagesInfo, er
6676 AddItem (nodeInfoBody , 0 , 5 , false ).
6777 AddItem (footer , 2 , 1 , false )
6878
79+ // Log viewer page (full-screen)
80+ // currentPage is captured by the closures below, so declare it here.
81+ dashPages := []string {pageStart , pageNodeInfo }
82+ currentPage := 0
83+
84+ var logViewer * widgets.LogViewerPage
85+ switchToDashboard := func () {
86+ currentPage = 1 // nodeInfo
87+ pages .SwitchToPage (pageNodeInfo )
88+ a .tview .SetFocus (pages )
89+ }
90+ logViewer = widgets .NewLogViewerPage (switchToDashboard )
91+ logsPage := tview .NewFlex ().SetDirection (tview .FlexRow ).
92+ AddItem (widgets .Header (), 1 , 1 , false ).
93+ AddItem (logViewer .Root , 0 , 1 , true )
94+
95+ // Seed the log tailer so the viewer has content immediately.
96+ a .logTailer .SeedFromEnd ()
97+
6998 // Start background goroutines
7099 go a .safeGo ("fillStagesInfo" , errCh , func () { a .fillStagesInfo (ctx , nodeView , infoCh ) })
71100 go a .safeGo ("runClock" , errCh , func () { a .runClock (ctx , nodeView .Clock ) })
@@ -74,25 +103,65 @@ func (a *App) Run(parent context.Context, infoCh <-chan *commands.StagesInfo, er
74103 go a .safeGo ("pollSystemHealth" , errCh , func () { a .pollSystemHealth (ctx , nodeView .SystemHealth ) })
75104 go a .safeGo ("pollAlerts" , errCh , func () { a .pollAlerts (ctx , nodeView .Alerts ) })
76105 go a .safeGo ("pollLogTail" , errCh , func () { a .pollLogTail (ctx , nodeView .LogTail ) })
106+ go a .safeGo ("pollLogViewer" , errCh , func () { a .pollLogViewer (ctx , logViewer ) })
77107
78- // Page navigation
79- currentPage , pagesCount := 0 , 2
80- names := []string {"start" , "nodeInfo" }
81- pages .AddPage (names [0 ], startPage , true , true )
82- pages .AddPage (names [1 ], nodeInfoPage , true , false )
108+ pages .AddPage (pageStart , startPage , true , true )
109+ pages .AddPage (pageNodeInfo , nodeInfoPage , true , false )
110+ pages .AddPage (pageLogs , logsPage , true , false )
83111
84112 if err := a .tview .SetRoot (pages , true ).EnableMouse (true ).SetInputCapture (
85113 func (event * tcell.EventKey ) * tcell.EventKey {
114+ currentFront , _ := pages .GetFrontPage ()
115+
116+ // --- Log viewer page input handling ---
117+ if currentFront == pageLogs {
118+ // When search bar is focused, let it handle all input except Escape.
119+ if logViewer .IsSearching () {
120+ if event .Key () == tcell .KeyEscape {
121+ logViewer .DismissSearch ()
122+ a .tview .SetFocus (logViewer .Content ())
123+ return nil
124+ }
125+ if event .Key () == tcell .KeyEnter {
126+ logViewer .DismissSearch ()
127+ a .tview .SetFocus (logViewer .Content ())
128+ return nil
129+ }
130+ return event // let InputField handle typing
131+ }
132+
133+ switch {
134+ case event .Key () == tcell .KeyCtrlC || event .Rune () == 'q' :
135+ cancel ()
136+ a .tview .Stop ()
137+ return nil
138+ case event .Key () == tcell .KeyEscape || event .Key () == tcell .KeyF1 :
139+ switchToDashboard ()
140+ return nil
141+ case event .Rune () == '/' :
142+ logViewer .EnterSearchMode ()
143+ a .tview .SetFocus (logViewer .SearchBar ())
144+ return nil
145+ }
146+ // Remaining keys (1-4, Space, arrows) handled by content's InputCapture.
147+ return event
148+ }
149+
150+ // --- Dashboard pages input handling ---
86151 switch {
87152 case event .Key () == tcell .KeyCtrlC || event .Rune () == 'q' :
88153 cancel ()
89154 a .tview .Stop ()
155+ case event .Key () == tcell .KeyF2 || event .Rune () == 'L' :
156+ pages .SwitchToPage (pageLogs )
157+ a .tview .SetFocus (logViewer .Content ())
158+ return nil
90159 case event .Key () == tcell .KeyRight :
91- currentPage = (currentPage + 1 + pagesCount ) % pagesCount
92- pages .SwitchToPage (names [currentPage ])
160+ currentPage = (currentPage + 1 ) % len ( dashPages )
161+ pages .SwitchToPage (dashPages [currentPage ])
93162 case event .Key () == tcell .KeyLeft :
94- currentPage = (currentPage - 1 + pagesCount ) % pagesCount
95- pages .SwitchToPage (names [currentPage ])
163+ currentPage = (currentPage - 1 + len ( dashPages )) % len ( dashPages )
164+ pages .SwitchToPage (dashPages [currentPage ])
96165 }
97166 return event
98167 }).Run (); err != nil {
@@ -346,3 +415,44 @@ func (a *App) pollLogTail(ctx context.Context, view *widgets.LogTailView) {
346415 }
347416 }
348417}
418+
419+ // pollLogViewer periodically tails the log file via LogTailer and updates
420+ // the full-screen log viewer widget.
421+ func (a * App ) pollLogViewer (ctx context.Context , viewer * widgets.LogViewerPage ) {
422+ const pollInterval = 500 * time .Millisecond
423+ ticker := time .NewTicker (pollInterval )
424+ defer ticker .Stop ()
425+
426+ var lastVersion int64
427+
428+ // Initial render from seeded data.
429+ lines := a .logTailer .Recent (logRingSize , viewer .FilterLevel ())
430+ a .tview .QueueUpdateDraw (func () {
431+ viewer .UpdateContent (lines )
432+ })
433+ lastVersion = a .logTailer .Version ()
434+
435+ for {
436+ select {
437+ case <- ctx .Done ():
438+ return
439+ case <- ticker .C :
440+ // Poll for new file content (I/O happens here, not on event loop).
441+ a .logTailer .Poll ()
442+
443+ v := a .logTailer .Version ()
444+ if v == lastVersion {
445+ continue
446+ }
447+ lastVersion = v
448+
449+ lines := a .logTailer .Recent (logRingSize , viewer .FilterLevel ())
450+ a .tview .QueueUpdateDraw (func () {
451+ viewer .UpdateContent (lines )
452+ })
453+ }
454+ }
455+ }
456+
457+ // logRingSize matches the datasource ring buffer size for the full viewer.
458+ const logRingSize = 1000
0 commit comments