Files
lerko cfbf01274d
CI / test (push) Successful in 2m40s
CI / lint (push) Successful in 1m2s
CI / vulncheck (push) Successful in 51s
Release / docker (push) Has been cancelled
Release / release (push) Has been cancelled
chore(tui): visual polish — detail sections, column headers, alert detail (#37)
## Summary

Bundled remaining UX polish items from the screenshot review.

### Changes

**Detail panel sections (#5)**
- Fields grouped into ENDPOINT, TIMING, HTTP, CONFIG sections with subtle headers
- Matches existing PROBE RESULTS and STATE CHANGES section pattern
- Cleaner visual hierarchy without box-drawing clutter

**Omit unconfigured fields (#6)**
- Timeout hidden when 0 (unconfigured)
- Method hidden when default GET
- AcceptedCodes shows "200-299" explicitly when empty

**Column header (#7)**
- `LATENCY` → `LAT` (design short, never truncate — htop/btop pattern)

**Alert detail view (#8)**
- `i` key on Alerts tab opens full detail panel
- Shows: type, health status, last sent time, send/fail counts, last error
- Full config key:value pairs (untruncated)
- Keybinding: `[i/Esc] Back  [e] Edit  [t] Test  [q] Quit`

### Files (3)
- `internal/tui/tab_sites.go` — section headers, field omission, LAT header
- `internal/tui/tab_alerts.go` — viewAlertDetailPanel()
- `internal/tui/tui.go` — stateAlertDetail, key handler, render routing

Reviewed-on: lerko/uptop#37
2026-05-28 20:40:29 +00:00

126 lines
2.6 KiB
Go

package tui
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
)
type userFormData struct {
Username string
PublicKey string
Role string
}
func fmtRole(role string) string {
if role == "admin" {
return specialStyle.Render(role)
}
return role
}
func fmtKey(key string) string {
if len(key) > 40 {
return key[:20] + "..." + key[len(key)-17:]
}
return key
}
func (m Model) viewUsersTab() string {
if len(m.users) == 0 {
return "\n No users configured. Press [n] to add one."
}
var headers []string
var widths []int
if m.isWide() {
headers = []string{"#", "USERNAME", "ROLE", "PUBLIC KEY"}
widths = []int{4, 18, 10, 50}
} else {
headers = []string{"#", "USER", "ROLE", "KEY"}
widths = []int{4, 14, 8, 30}
}
userW := widths[1]
return m.renderTable(
headers,
len(m.users),
func(start, end int) [][]string {
var rows [][]string
for i := start; i < end; i++ {
u := m.users[i]
rows = append(rows, []string{
fmt.Sprintf("%d", i+1),
m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, userW-2)),
fmtRole(u.Role),
fmtKey(u.PublicKey),
})
}
return rows
},
widths, nil,
)
}
func (m *Model) initUserHuhForm() tea.Cmd {
m.userFormData = &userFormData{
Role: "user",
}
if m.editID > 0 {
for _, u := range m.users {
if u.ID == m.editID {
m.userFormData.Username = u.Username
m.userFormData.PublicKey = u.PublicKey
m.userFormData.Role = u.Role
break
}
}
}
m.huhForm = huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Username").
Placeholder("admin").
Value(&m.userFormData.Username).
Validate(func(s string) error {
if s == "" {
return fmt.Errorf("username is required")
}
return nil
}),
huh.NewInput().Title("SSH Public Key").
Placeholder("ssh-ed25519 AAAA...").
Value(&m.userFormData.PublicKey).
Validate(func(s string) error {
if s == "" {
return fmt.Errorf("public key is required")
}
return nil
}),
huh.NewSelect[string]().Title("Role").
Options(
huh.NewOption("User", "user"),
huh.NewOption("Admin", "admin"),
).Value(&m.userFormData.Role),
).Title("SSH Access"),
).WithTheme(m.theme.HuhTheme())
return m.huhForm.Init()
}
func (m *Model) submitUserForm() {
d := m.userFormData
if m.editID > 0 {
if err := m.store.UpdateUser(m.editID, d.Username, d.PublicKey, d.Role); err != nil {
m.engine.AddLog("Update user failed: " + err.Error())
}
} else {
if err := m.store.AddUser(d.Username, d.PublicKey, d.Role); err != nil {
m.engine.AddLog("Add user failed: " + err.Error())
}
}
m.state = stateUsers
}