@@ -806,7 +806,10 @@ type Model struct {
806806 dbDetailDatabases []map [string ]interface {}
807807 dbDetailPools []map [string ]interface {}
808808 dbDetailLoaded bool // true once fetchDBDetailSubresources has returned
809- dbDetailTab int // 0=Service, 1=Users, 2=Backups, 3=Databases, 4=Pools
809+ dbDetailTab int // 0=Service, 1=Users, 2=Backups, 3=Databases, 4=Pools, 5=Logs
810+ dbDetailLogs []map [string ]interface {} // last fetched log entries
811+ dbLogsLoaded bool // true once fetchDBLogs has returned
812+ dbLogsScrollOffset int // scroll offset for logs tab (0 = bottom/newest)
810813 // DB user state (Users tab)
811814 dbUserCreateMode bool // true when typing a new username
812815 dbUserCreateInput string // username being typed
@@ -954,6 +957,9 @@ type setDefaultProjectMsg struct {
954957// clearNotificationMsg is sent to clear the notification after timeout
955958type clearNotificationMsg struct {}
956959
960+ // dbLogsRefreshTickMsg triggers a periodic re-fetch of DB logs
961+ type dbLogsRefreshTickMsg struct {}
962+
957963// refreshTickMsg is sent to trigger automatic refresh of data
958964type refreshTickMsg struct {}
959965
@@ -2666,6 +2672,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
26662672 m .dbDBCreateMode = false
26672673 m .dbDBCreateInput = ""
26682674 m .dbPoolCreateStep = - 1
2675+ m .dbDetailLogs = nil
2676+ m .dbLogsLoaded = false
2677+ m .dbLogsScrollOffset = 0
26692678 m .mode = LoadingView
26702679 path := "/databases"
26712680 if m .currentProduct == ProductManagedAnalytics {
@@ -2766,6 +2775,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
27662775 }
27672776 return m , tea .Tick (4 * time .Second , func (t time.Time ) tea.Msg { return clearNotificationMsg {} })
27682777
2778+ case dbLogsMsg :
2779+ m .dbLogsLoaded = true
2780+ if msg .err != nil {
2781+ m .dbDetailLogs = nil
2782+ m .notification = fmt .Sprintf ("❌ Failed to fetch logs: %s" , msg .err .Error ())
2783+ m .notificationExpiry = time .Now ().Add (8 * time .Second )
2784+ return m , tea .Tick (8 * time .Second , func (t time.Time ) tea.Msg { return clearNotificationMsg {} })
2785+ }
2786+ m .dbDetailLogs = msg .logs
2787+ // Schedule next auto-refresh in 10 s
2788+ return m , tea .Tick (10 * time .Second , func (t time.Time ) tea.Msg { return dbLogsRefreshTickMsg {} })
2789+
2790+ case dbLogsRefreshTickMsg :
2791+ // Only re-fetch if still viewing the Logs tab
2792+ if m .mode == DetailView &&
2793+ (m .currentProduct == ProductManagedDatabases || m .currentProduct == ProductManagedAnalytics ) &&
2794+ m .dbDetailTab == 5 {
2795+ return m , m .fetchDBLogs ()
2796+ }
2797+ // Tab was left — stop the loop
2798+ return m , nil
2799+
27692800 case dbCreatedMsg :
27702801 m .wizard .isLoading = false
27712802 m .wizard .loadingMessage = ""
@@ -7250,7 +7281,7 @@ func (m Model) renderManagedDatabaseDetail(width int) string {
72507281 Foreground (lipgloss .Color ("#444444" )).
72517282 Padding (0 , 2 )
72527283
7253- tabNames := []string {"Service" , "Users" , "Backups" , "Databases" , "Pools" }
7284+ tabNames := []string {"Service" , "Users" , "Backups" , "Databases" , "Pools" , "Logs" }
72547285 var tabParts []string
72557286 for i , name := range tabNames {
72567287 if i == 4 && ! isPostgres {
@@ -7690,6 +7721,66 @@ func (m Model) renderManagedDatabaseDetail(width int) string {
76907721 }
76917722 content .WriteString (renderBox (fmt .Sprintf ("Connection Pools (%d)" , len (m .dbDetailPools )), strings .TrimRight (poolsContent .String (), "\n " ), fullWidth ))
76927723 }
7724+
7725+ case 5 : // ── Logs ───────────────────────────────────────────────────
7726+ refreshBtn := lipgloss .NewStyle ().
7727+ Foreground (lipgloss .Color ("#7B68EE" )).Bold (true ).Padding (0 , 1 ).Render ("[↻ Refresh]" )
7728+ liveIndicator := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#00FF7F" )).Render ("● live" )
7729+ content .WriteString (refreshBtn + " " + dimSt .Render ("Enter → reload" ) + " " + liveIndicator + dimSt .Render (" (auto every 10s)" ) + "\n \n " )
7730+
7731+ var logsContent strings.Builder
7732+ if ! m .dbLogsLoaded {
7733+ logsContent .WriteString (dimSt .Render ("Loading..." ))
7734+ } else if len (m .dbDetailLogs ) == 0 {
7735+ logsContent .WriteString (dimSt .Render ("No log entries found" ))
7736+ } else {
7737+ timeSt := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#888888" ))
7738+ hostSt := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#7B68EE" ))
7739+ msgSt := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#FFFFFF" ))
7740+ indentSt := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#666666" ))
7741+ // Derive maxVisible from terminal height.
7742+ // Each entry uses 2 lines (header + message), so halve available lines.
7743+ maxVisible := (m .height - 27 ) / 2
7744+ if maxVisible < 3 {
7745+ maxVisible = 3
7746+ }
7747+ // Show entries most-recent first, with scroll window
7748+ total := len (m .dbDetailLogs )
7749+ startIdx := m .dbLogsScrollOffset
7750+ if startIdx > total - 1 {
7751+ startIdx = total - 1
7752+ }
7753+ if startIdx < 0 {
7754+ startIdx = 0
7755+ }
7756+ endIdx := startIdx + maxVisible
7757+ if endIdx > total {
7758+ endIdx = total
7759+ }
7760+ scrollHint := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#444444" )).Render (
7761+ fmt .Sprintf (" ↑/↓ scroll (%d-%d / %d)" , startIdx + 1 , endIdx , total ))
7762+ logsContent .WriteString (scrollHint + "\n " )
7763+ // iterate reversed (newest = index 0)
7764+ for ri := startIdx ; ri < endIdx ; ri ++ {
7765+ i := total - 1 - ri // actual index in m.dbDetailLogs
7766+ e := m .dbDetailLogs [i ]
7767+ msgStr := getStringValue (e , "message" , "" )
7768+ host := getStringValue (e , "hostname" , "" )
7769+ ts := ""
7770+ if v , ok := toFloat64 (e ["timestamp" ]); ok {
7771+ t2 := time .Unix (int64 (v ), 0 ).UTC ()
7772+ ts = t2 .Format ("2006-01-02 15:04:05" )
7773+ }
7774+ // Line 1: timestamp hostname
7775+ logsContent .WriteString (timeSt .Render (ts ) + " " + hostSt .Render (host ) + "\n " )
7776+ // Line 2: indented full message
7777+ logsContent .WriteString (indentSt .Render (" ↳ " ) + msgSt .Render (msgStr ) + "\n " )
7778+ }
7779+ }
7780+ content .WriteString (renderBox (
7781+ fmt .Sprintf ("Logs (%d)" , len (m .dbDetailLogs )),
7782+ strings .TrimRight (logsContent .String (), "\n " ),
7783+ fullWidth ))
76937784 }
76947785
76957786 return content .String ()
@@ -9533,6 +9624,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
95339624 m .dbUserSelectedIdx = - 1
95349625 m .dbUserDeleteConfirm = false
95359626 m .dbPoolCreateStep = - 1
9627+ m .dbLogsScrollOffset = 0
95369628 }
95379629 return m , nil
95389630 }
@@ -9694,13 +9786,20 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
96949786 }
96959787 // In DetailView for ManagedDatabases/Analytics, → switches tabs
96969788 if m .mode == DetailView && (m .currentProduct == ProductManagedDatabases || m .currentProduct == ProductManagedAnalytics ) {
9697- maxTab := 4
9789+ maxTab := 5
96989790 if m .dbDetailTab < maxTab {
96999791 m .dbDetailTab ++
97009792 m .actionConfirm = false
97019793 m .dbUserSelectedIdx = - 1
97029794 m .dbUserDeleteConfirm = false
97039795 m .dbPoolCreateStep = - 1
9796+ m .dbLogsScrollOffset = 0
9797+ // Entering Logs tab: trigger fetch
9798+ if m .dbDetailTab == 5 {
9799+ m .dbLogsLoaded = false
9800+ m .dbDetailLogs = nil
9801+ return m , m .fetchDBLogs ()
9802+ }
97049803 }
97059804 return m , nil
97069805 }
@@ -10580,6 +10679,11 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
1058010679 m .dbPoolCreateMode = 0
1058110680 m .dbPoolCreateSize = "10"
1058210681 }
10682+ case 5 : // Logs tab — refresh
10683+ m .dbLogsLoaded = false
10684+ m .dbDetailLogs = nil
10685+ m .dbLogsScrollOffset = 0
10686+ return m , m .fetchDBLogs ()
1058310687 }
1058410688 return m , nil
1058510689 } else if m .mode == DetailView && m .currentProduct == ProductInstances {
@@ -10976,6 +11080,9 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
1097611080 m .dbDBCreateMode = false
1097711081 m .dbDBCreateInput = ""
1097811082 m .dbPoolCreateStep = - 1
11083+ m .dbDetailLogs = nil
11084+ m .dbLogsLoaded = false
11085+ m .dbLogsScrollOffset = 0
1097911086 if engine != "" && serviceId != "" {
1098011087 return m , m .fetchDBDetailSubresources (engine , serviceId )
1098111088 }
@@ -10992,6 +11099,27 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
1099211099 isSubNavProduct := isStorageSubProduct || isNetworkSubProduct || isComputeSubProduct
1099311100 navItems := getNavItems ()
1099411101
11102+ // DB Logs tab: ↑/↓ scroll
11103+ if m .mode == DetailView && (m .currentProduct == ProductManagedDatabases || m .currentProduct == ProductManagedAnalytics ) &&
11104+ m .dbDetailTab == 5 && m .dbLogsLoaded {
11105+ total := len (m .dbDetailLogs )
11106+ maxVisible := 20
11107+ maxOffset := total - maxVisible
11108+ if maxOffset < 0 {
11109+ maxOffset = 0
11110+ }
11111+ if key == "up" || key == "k" {
11112+ if m .dbLogsScrollOffset < maxOffset {
11113+ m .dbLogsScrollOffset ++
11114+ }
11115+ } else if key == "down" || key == "j" {
11116+ if m .dbLogsScrollOffset > 0 {
11117+ m .dbLogsScrollOffset --
11118+ }
11119+ }
11120+ return m , nil
11121+ }
11122+
1099511123 // DB Users tab: ↑/↓ navigate user rows
1099611124 if m .mode == DetailView && (m .currentProduct == ProductManagedDatabases || m .currentProduct == ProductManagedAnalytics ) &&
1099711125 m .dbDetailTab == 1 && ! m .dbUserCreateMode && m .dbUserCreatedData == nil {
0 commit comments