Skip to content

Commit 66d7d47

Browse files
committed
fix(server): TUI admin panel layout, logs view, and heading alignment
1 parent 83175e0 commit 66d7d47

2 files changed

Lines changed: 98 additions & 69 deletions

File tree

ARCHITECTURE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,13 @@ The TUI-based admin panel provides real-time server management:
325325
- **Database Operations**: Backup, restore, and maintenance functions
326326
- **System Controls**: Force garbage collection and metrics reset
327327

328+
Implementation notes (`server/admin_panel.go`):
329+
330+
- **Resize and layout**: Window size updates apply width and height through a single layout path so the help bar, user table, and plugin table stay in sync; content width is derived consistently for bordered panels.
331+
- **Tabs**: Tab labels render at natural width with spacing instead of fixed equal-width cells, which reads better on narrow terminals.
332+
- **Logs tab**: Log lines are ordered oldest-first so the newest entries appear at the bottom; when output exceeds the visible area, the view initially anchors to the bottom of the buffer.
333+
- **Headings**: Section titles and info lines avoid embedding trailing newlines inside styled strings (newlines are written separately) so System and Metrics panels stay left-aligned.
334+
328335
### Web Admin Panel
329336

330337
The web-based interface provides the same functionality through a browser:

server/admin_panel.go

Lines changed: 91 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,8 @@ func NewAdminPanel(hub *Hub, db *sql.DB, pluginManager *manager.PluginManager, l
448448
selectedPlugin: -1,
449449
}
450450

451+
panel.applyLayout(120, 40)
452+
451453
// Load initial data
452454
panel.refreshData()
453455

@@ -788,16 +790,7 @@ func (ap *AdminPanel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
788790

789791
switch msg := msg.(type) {
790792
case tea.WindowSizeMsg:
791-
ap.width = msg.Width
792-
ap.height = msg.Height
793-
794-
availableWidth := msg.Width - 12
795-
if availableWidth < 30 {
796-
availableWidth = 30
797-
}
798-
799-
ap.help.Width = availableWidth
800-
ap.userTable.SetWidth(availableWidth)
793+
ap.applyLayout(msg.Width, msg.Height)
801794

802795
case tea.KeyMsg:
803796
switch {
@@ -1055,15 +1048,40 @@ func (ap *AdminPanel) renderScrollableContent(content string, scrollOffset int)
10551048
return strings.Join(visibleLines, "\n")
10561049
}
10571050

1051+
func (ap *AdminPanel) contentWidth() int {
1052+
width := ap.width - 12
1053+
if width < 30 {
1054+
return 30
1055+
}
1056+
return width
1057+
}
1058+
1059+
func (ap *AdminPanel) applyLayout(width, height int) {
1060+
ap.width = width
1061+
ap.height = height
1062+
1063+
contentWidth := ap.contentWidth()
1064+
ap.help.Width = contentWidth
1065+
ap.userTable.SetWidth(contentWidth)
1066+
ap.pluginTable.SetWidth(contentWidth)
1067+
1068+
usableHeight := height - 16
1069+
if usableHeight < 6 {
1070+
usableHeight = 6
1071+
}
1072+
if usableHeight > 18 {
1073+
usableHeight = 18
1074+
}
1075+
ap.userTable.SetHeight(usableHeight)
1076+
ap.pluginTable.SetHeight(usableHeight)
1077+
}
1078+
10581079
func (ap *AdminPanel) View() string {
10591080
if ap.quitting {
10601081
return "Admin panel closed. Server continues running.\n"
10611082
}
10621083

1063-
availableWidth := ap.width - 12
1064-
if availableWidth < 30 {
1065-
availableWidth = 30
1066-
}
1084+
availableWidth := ap.contentWidth()
10671085

10681086
doc := strings.Builder{}
10691087

@@ -1094,15 +1112,6 @@ func (ap *AdminPanel) View() string {
10941112
func (ap *AdminPanel) renderTabs() string {
10951113
var renderedTabs []string
10961114

1097-
availableWidth := ap.width - 12
1098-
if availableWidth < 30 {
1099-
availableWidth = 30
1100-
}
1101-
tabWidth := availableWidth / len(ap.tabs)
1102-
if tabWidth < 8 {
1103-
tabWidth = 8
1104-
}
1105-
11061115
for i, tab := range ap.tabs {
11071116
var style lipgloss.Style
11081117
if i == int(ap.activeTab) {
@@ -1111,11 +1120,11 @@ func (ap *AdminPanel) renderTabs() string {
11111120
style = tabStyle
11121121
}
11131122

1114-
renderedTab := style.Width(tabWidth).Align(lipgloss.Center).Render(tab)
1123+
renderedTab := style.Render(tab)
11151124
renderedTabs = append(renderedTabs, renderedTab)
11161125
}
11171126

1118-
return lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
1127+
return strings.Join(renderedTabs, " ")
11191128
}
11201129

11211130
func (ap *AdminPanel) renderContent() string {
@@ -1140,13 +1149,11 @@ func (ap *AdminPanel) renderContent() string {
11401149
func (ap *AdminPanel) renderOverview() string {
11411150
doc := strings.Builder{}
11421151

1143-
contentWidth := ap.width - 12
1144-
if contentWidth < 30 {
1145-
contentWidth = 30
1146-
}
1152+
contentWidth := ap.contentWidth()
11471153

11481154
// System status
1149-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("System Status\n"))
1155+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("System Status"))
1156+
doc.WriteString("\n")
11501157
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
11511158

11521159
statusText := "🟢 " + ap.systemInfo.ServerStatus
@@ -1162,7 +1169,8 @@ func (ap *AdminPanel) renderOverview() string {
11621169
doc.WriteString("\n")
11631170

11641171
// Live Configuration Summary
1165-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Live Configuration\n"))
1172+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Live Configuration"))
1173+
doc.WriteString("\n")
11661174
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
11671175
doc.WriteString(fmt.Sprintf("Port: %d\n", ap.config.Port))
11681176

@@ -1181,7 +1189,8 @@ func (ap *AdminPanel) renderOverview() string {
11811189
doc.WriteString("\n")
11821190

11831191
// Database info
1184-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Database Information\n"))
1192+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Database Information"))
1193+
doc.WriteString("\n")
11851194
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
11861195
doc.WriteString(fmt.Sprintf("Database Path: %s\n", ap.config.DBPath))
11871196
doc.WriteString(fmt.Sprintf("Config Directory: %s\n", ap.config.ConfigDir))
@@ -1192,12 +1201,10 @@ func (ap *AdminPanel) renderOverview() string {
11921201
func (ap *AdminPanel) renderUsers() string {
11931202
doc := strings.Builder{}
11941203

1195-
contentWidth := ap.width - 12
1196-
if contentWidth < 30 {
1197-
contentWidth = 30
1198-
}
1204+
contentWidth := ap.contentWidth()
11991205

1200-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("User Management\n"))
1206+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("User Management"))
1207+
doc.WriteString("\n")
12011208
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
12021209

12031210
// Show selected user info
@@ -1237,18 +1244,18 @@ func (ap *AdminPanel) renderUsers() string {
12371244
func (ap *AdminPanel) renderSystem() string {
12381245
doc := strings.Builder{}
12391246

1240-
contentWidth := ap.width - 12
1241-
if contentWidth < 30 {
1242-
contentWidth = 30
1243-
}
1247+
contentWidth := ap.contentWidth()
12441248

1245-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("System Management\n"))
1249+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("System Management"))
1250+
doc.WriteString("\n")
12461251
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
12471252

1248-
doc.WriteString(infoStylePanel.Render("Use [c] Clear Database, [b] Backup Database, [s] Show Stats\n\n"))
1253+
doc.WriteString(infoStylePanel.Render("Use [c] Clear Database, [b] Backup Database, [s] Show Stats"))
1254+
doc.WriteString("\n\n")
12491255

12501256
// Live Configuration Details
1251-
doc.WriteString(subtitleStyle.Render("Live Configuration:\n"))
1257+
doc.WriteString(subtitleStyle.Render("Live Configuration:"))
1258+
doc.WriteString("\n")
12521259
doc.WriteString(fmt.Sprintf(" Server Port: %d\n", ap.config.Port))
12531260
doc.WriteString(fmt.Sprintf(" Database: %s\n", ap.config.DBPath))
12541261
doc.WriteString(fmt.Sprintf(" Config Directory: %s\n", ap.config.ConfigDir))
@@ -1276,7 +1283,8 @@ func (ap *AdminPanel) renderSystem() string {
12761283
doc.WriteString(fmt.Sprintf(" Plugin Registry: %s\n", ap.config.PluginRegistryURL))
12771284

12781285
doc.WriteString("\n")
1279-
doc.WriteString(subtitleStyle.Render("Database Statistics:\n"))
1286+
doc.WriteString(subtitleStyle.Render("Database Statistics:"))
1287+
doc.WriteString("\n")
12801288
doc.WriteString(fmt.Sprintf(" Total Messages: %d\n", ap.systemInfo.MessagesSent))
12811289
doc.WriteString(fmt.Sprintf(" Total Users: %d\n", ap.systemInfo.TotalUsers))
12821290
doc.WriteString(fmt.Sprintf(" Active Connections: %d\n", ap.systemInfo.ActiveUsers))
@@ -1288,16 +1296,15 @@ func (ap *AdminPanel) renderSystem() string {
12881296
func (ap *AdminPanel) renderLogs() string {
12891297
doc := strings.Builder{}
12901298

1291-
contentWidth := ap.width - 12
1292-
if contentWidth < 30 {
1293-
contentWidth = 30
1294-
}
1299+
contentWidth := ap.contentWidth()
12951300

1296-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("System Logs\n"))
1301+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("System Logs"))
1302+
doc.WriteString("\n")
12971303
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
12981304

1299-
// Add logs content
1300-
for _, logEntry := range ap.logs {
1305+
// Add logs content (oldest first so newest appear at the bottom)
1306+
for i := len(ap.logs) - 1; i >= 0; i-- {
1307+
logEntry := ap.logs[i]
13011308
var levelStyle lipgloss.Style
13021309
switch logEntry.Level {
13031310
case "ERROR":
@@ -1313,19 +1320,30 @@ func (ap *AdminPanel) renderLogs() string {
13131320
logEntry.Component,
13141321
logEntry.Message))
13151322
}
1323+
content := doc.String()
1324+
lines := strings.Split(content, "\n")
1325+
availableHeight := ap.height - 8
1326+
if availableHeight < 10 {
1327+
availableHeight = 10
1328+
}
1329+
maxLines := availableHeight - 2
1330+
if maxLines < 1 {
1331+
maxLines = 1
1332+
}
1333+
if ap.logsScroll == 0 && len(lines) > maxLines {
1334+
ap.logsScroll = len(lines) - maxLines
1335+
}
13161336

1317-
return ap.renderScrollableContent(doc.String(), ap.logsScroll)
1337+
return ap.renderScrollableContent(content, ap.logsScroll)
13181338
}
13191339

13201340
func (ap *AdminPanel) renderPlugins() string {
13211341
doc := strings.Builder{}
13221342

1323-
contentWidth := ap.width - 12
1324-
if contentWidth < 30 {
1325-
contentWidth = 30
1326-
}
1343+
contentWidth := ap.contentWidth()
13271344

1328-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Plugin Management\n"))
1345+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Plugin Management"))
1346+
doc.WriteString("\n")
13291347
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
13301348

13311349
// Show selected plugin info
@@ -1400,18 +1418,18 @@ func (ap *AdminPanel) renderPlugins() string {
14001418
func (ap *AdminPanel) renderMetrics() string {
14011419
doc := strings.Builder{}
14021420

1403-
contentWidth := ap.width - 12
1404-
if contentWidth < 30 {
1405-
contentWidth = 30
1406-
}
1421+
contentWidth := ap.contentWidth()
14071422

1408-
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Performance Metrics\n"))
1423+
doc.WriteString(subtitleStyle.Width(contentWidth).Render("Performance Metrics"))
1424+
doc.WriteString("\n")
14091425
doc.WriteString(strings.Repeat("─", min(20, contentWidth-2)) + "\n")
14101426

1411-
doc.WriteString(infoStylePanel.Render("Use [G] Force GC, [R] Reset Metrics, [E] Export Logs\n\n"))
1427+
doc.WriteString(infoStylePanel.Render("Use [G] Force GC, [R] Reset Metrics, [E] Export Logs"))
1428+
doc.WriteString("\n\n")
14121429

14131430
// System Performance - more compact layout
1414-
doc.WriteString(metricLabelStyle.Render("System Performance:\n"))
1431+
doc.WriteString(metricLabelStyle.Render("System Performance:"))
1432+
doc.WriteString("\n")
14151433
doc.WriteString(fmt.Sprintf("Memory: %s | Goroutines: %s | Heap: %s\n",
14161434
metricValueStyle.Render(fmt.Sprintf("%.1f MB", ap.systemInfo.MemoryUsage)),
14171435
metricValueStyle.Render(fmt.Sprintf("%d", ap.systemInfo.GoroutineCount)),
@@ -1423,7 +1441,8 @@ func (ap *AdminPanel) renderMetrics() string {
14231441
doc.WriteString("\n")
14241442

14251443
// Connection Metrics - more compact layout
1426-
doc.WriteString(metricLabelStyle.Render("Connection Metrics:\n"))
1444+
doc.WriteString(metricLabelStyle.Render("Connection Metrics:"))
1445+
doc.WriteString("\n")
14271446
doc.WriteString(fmt.Sprintf("Active: %s | Peak: %s | Total: %s | Disconnects: %s\n",
14281447
metricValueStyle.Render(fmt.Sprintf("%d", ap.systemInfo.ActiveUsers)),
14291448
metricValueStyle.Render(fmt.Sprintf("%d", ap.metrics.PeakUsers)),
@@ -1433,7 +1452,8 @@ func (ap *AdminPanel) renderMetrics() string {
14331452
doc.WriteString("\n")
14341453

14351454
// Message Metrics - more compact layout
1436-
doc.WriteString(metricLabelStyle.Render("Message Metrics:\n"))
1455+
doc.WriteString(metricLabelStyle.Render("Message Metrics:"))
1456+
doc.WriteString("\n")
14371457
doc.WriteString(fmt.Sprintf("Total: %s | Rate: %s | Conn Rate: %s\n",
14381458
metricValueStyle.Render(fmt.Sprintf("%d", ap.systemInfo.MessagesSent)),
14391459
metricValueStyle.Render(fmt.Sprintf("%.2f msg/s", ap.messageRate)),
@@ -1443,7 +1463,8 @@ func (ap *AdminPanel) renderMetrics() string {
14431463

14441464
// Memory History Chart (simplified)
14451465
if len(ap.metrics.MemoryHistory) > 0 {
1446-
doc.WriteString(metricLabelStyle.Render("Memory History (last 5):\n"))
1466+
doc.WriteString(metricLabelStyle.Render("Memory History (last 5):"))
1467+
doc.WriteString("\n")
14471468
recent := ap.metrics.MemoryHistory
14481469
if len(recent) > 5 {
14491470
recent = recent[len(recent)-5:]
@@ -1459,7 +1480,8 @@ func (ap *AdminPanel) renderMetrics() string {
14591480

14601481
// Connection History Chart (simplified)
14611482
if len(ap.metrics.ConnectionHistory) > 0 {
1462-
doc.WriteString(metricLabelStyle.Render("Connection History (last 5):\n"))
1483+
doc.WriteString(metricLabelStyle.Render("Connection History (last 5):"))
1484+
doc.WriteString("\n")
14631485
recent := ap.metrics.ConnectionHistory
14641486
if len(recent) > 5 {
14651487
recent = recent[len(recent)-5:]

0 commit comments

Comments
 (0)