package main import ( "context" "crypto/tls" "encoding/json" "fmt" "io" "os" "strings" "time" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" pbhuman "github.com/flowy-live/admin-cli/genproto/helios/human" pbkeypad "github.com/flowy-live/admin-cli/genproto/helios/keypad" "github.com/mergestat/timediff" "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "net/http" ) type MetricResponse struct { Metrics []struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Value float64 `json:"value"` Unit string `json:"unit"` Period string `json:"period"` } `json:"metrics"` } func getRevenue() string { client := &http.Client{} req, err := http.NewRequest("GET", "https://api.revenuecat.com/v2/projects/proj5b2f7356/metrics/overview", nil) if err != nil { return fmt.Sprintf("Error creating request: %v", err) } // Add authorization header req.Header.Add("Authorization", "Bearer sk_wxmjNCLtnDSUPmSXlbEWyuqkpTVTi") response, err := client.Do(req) if err != nil { return fmt.Sprintf("Error fetching revenue: %v", err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { return fmt.Sprintf("Error reading response: %v", err) } var metricResp MetricResponse err = json.Unmarshal(body, &metricResp) if err != nil { return fmt.Sprintf("Error parsing JSON: %v", err) } // Create a map for easy lookup metrics := make(map[string]struct { name string value float64 unit string }) for _, m := range metricResp.Metrics { metrics[m.ID] = struct { name string value float64 unit string }{ name: m.Name, value: m.Value, unit: m.Unit, } } if m, ok := metrics["revenue"]; ok { return fmt.Sprintf("%s%.0f", m.unit, m.value) } return "" // Format the output // var result strings.Builder // result.WriteString("\n") // result.WriteString("šŸ’° Revenue Metrics\n") // result.WriteString("═══════════════════════════════════════\n\n") // Key metrics // if m, ok := metrics["mrr"]; ok { // result.WriteString(fmt.Sprintf("MRR: %s%.0f\n", m.unit, m.value)) // } // if m, ok := metrics["revenue"]; ok { // result.WriteString(fmt.Sprintf("Revenue (28d): %s%.0f\n", m.unit, m.value)) // } // result.WriteString("\n") // // if m, ok := metrics["active_subscriptions"]; ok { // result.WriteString(fmt.Sprintf("Active Subscriptions: %s%.0f\n", m.unit, m.value)) // } // if m, ok := metrics["active_trials"]; ok { // result.WriteString(fmt.Sprintf("Active Trials: %s%.0f\n", m.unit, m.value)) // } // result.WriteString("\n") // // if m, ok := metrics["new_customers"]; ok { // result.WriteString(fmt.Sprintf("New Customers (28d): %s%.0f\n", m.unit, m.value)) // } // if m, ok := metrics["active_users"]; ok { // result.WriteString(fmt.Sprintf("Active Users (28d): %s%.0f\n", m.unit, m.value)) // } // // result.WriteString("\n═══════════════════════════════════════\n") // // return result.String() } var ( titleStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#7D56F4")). Padding(0, 1). MarginBottom(1) selectedItemStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#7D56F4")). Bold(true). Padding(0, 1) normalItemStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FAFAFA")). Padding(0, 1) dimItemStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#666666")). Padding(0, 1) cursorStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FF75B5")). Bold(true) helpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#626262")). Padding(1, 0, 0, 1) statusStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#7D56F4")). Bold(true) borderStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#3C3C3C")) timestampStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")). Italic(true) selectedTimestampStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#CCCCCC")). Italic(true) detailPanelStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#7D56F4")). Padding(1, 2) detailTitleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#7D56F4")). Bold(true). Underline(true) detailLabelStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")). Bold(true) detailValueStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FAFAFA")) codeBlockStyle = lipgloss.NewStyle(). Background(lipgloss.Color("#1a1a1a")). Foreground(lipgloss.Color("#00FF00")). Padding(1, 1). MarginTop(1) ) type HumanData struct { Email string DisplayName string JoinedAt time.Time KeypadConfiguration string } type model struct { humans []*HumanData cursor int selectedHumanEmail string viewport viewport.Model detailViewport viewport.Model ready bool terminalWidth int terminalHeight int focusOnDetail bool // true = detail panel has focus, false = list has focus revenue string } func initialModel(humans []*HumanData) model { return model{ humans: humans, cursor: 0, revenue: getRevenue(), } } func (m model) Init() tea.Cmd { // Just return `nil`, which means "no I/O right now, please." return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.terminalWidth = msg.Width m.terminalHeight = msg.Height headerHeight := 3 // title + status + separator footerHeight := 2 // help text verticalMarginHeight := headerHeight + footerHeight // Split width: 50% for list, 50% for details listWidth := msg.Width / 2 detailWidth := msg.Width - listWidth - 3 // -3 for padding/border if !m.ready { m.viewport = viewport.New(listWidth, msg.Height-verticalMarginHeight) m.viewport.YPosition = headerHeight m.viewport.SetContent(m.renderList()) m.detailViewport = viewport.New(detailWidth, msg.Height-verticalMarginHeight) m.detailViewport.YPosition = headerHeight m.detailViewport.SetContent(m.renderDetails()) m.ready = true } else { m.viewport.Width = listWidth m.viewport.Height = msg.Height - verticalMarginHeight m.detailViewport.Width = detailWidth m.detailViewport.Height = msg.Height - verticalMarginHeight } case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "ctrl+u": // Scroll detail panel up only m.detailViewport.LineUp(3) return m, nil case "ctrl+d": // Scroll detail panel down only m.detailViewport.LineDown(3) return m, nil case "up", "k": if m.cursor > 0 { m.cursor-- m.viewport.SetContent(m.renderList()) m.detailViewport.SetContent(m.renderDetails()) m.detailViewport.GotoTop() // Reset detail scroll position // Auto-scroll left viewport to keep cursor visible if m.cursor < m.viewport.YOffset { m.viewport.YOffset = m.cursor } } case "r": m.humans = getHumansData() m.cursor = 0 m.viewport.SetContent(m.renderList()) m.detailViewport.SetContent(m.renderDetails()) m.detailViewport.GotoTop() case "down", "j": if m.cursor < len(m.humans)-1 { m.cursor++ m.viewport.SetContent(m.renderList()) m.detailViewport.SetContent(m.renderDetails()) m.detailViewport.GotoTop() // Reset detail scroll position // Auto-scroll left viewport to keep cursor visible if m.cursor >= m.viewport.YOffset+m.viewport.Height { m.viewport.YOffset = m.cursor - m.viewport.Height + 1 } } } } return m, nil } func (m model) renderList() string { var b strings.Builder for i, human := range m.humans { email := human.Email // Get relative time ago timeAgo := timediff.TimeDiff(human.JoinedAt) if m.cursor == i { // Current selection - highlighted cursor := cursorStyle.Render("ā–ø") item := selectedItemStyle.Render(email) timestamp := selectedTimestampStyle.Render(fmt.Sprintf("(%s)", timeAgo)) b.WriteString(fmt.Sprintf("%s %s %s\n", cursor, item, timestamp)) } else { // Normal item cursor := dimItemStyle.Render(" ") item := normalItemStyle.Render(email) timestamp := timestampStyle.Render(fmt.Sprintf("(%s)", timeAgo)) b.WriteString(fmt.Sprintf("%s %s %s\n", cursor, item, timestamp)) } } return b.String() } func (m model) renderDetails() string { if len(m.humans) == 0 || m.cursor >= len(m.humans) { return "" } human := m.humans[m.cursor] var b strings.Builder // Title b.WriteString(detailTitleStyle.Render("Human Details") + "\n\n") // Email b.WriteString(detailLabelStyle.Render("Email: ")) b.WriteString(detailValueStyle.Render(human.Email) + "\n\n") // Display Name if human.DisplayName != "" { b.WriteString(detailLabelStyle.Render("Display Name: ")) b.WriteString(detailValueStyle.Render(human.DisplayName) + "\n\n") } // Joined At b.WriteString(detailLabelStyle.Render("Joined: ")) timeAgo := timediff.TimeDiff(human.JoinedAt) joinedFormatted := human.JoinedAt.Format("Jan 2, 2006 at 3:04 PM") b.WriteString(detailValueStyle.Render(fmt.Sprintf("%s (%s)", joinedFormatted, timeAgo)) + "\n\n") // Keypad Configuration if human.KeypadConfiguration != "" { b.WriteString(detailLabelStyle.Render("Keypad Configuration:") + "\n") b.WriteString(codeBlockStyle.Render(human.KeypadConfiguration)) } else { b.WriteString(detailLabelStyle.Render("Keypad Configuration: ")) b.WriteString(dimItemStyle.Render("(none)")) } return b.String() } func (m model) View() string { if !m.ready { return "\n Initializing..." } // Header with main data sevenDaysAgo := time.Now().AddDate(0, 0, -7) pastSevenDaysSignupsCount := 0 for _, human := range m.humans { if human.JoinedAt.After(sevenDaysAgo) { pastSevenDaysSignupsCount++ } } header := titleStyle.Render(fmt.Sprintf("šŸ‘„ Humans | %d total | %d in the past 7 days | Revenue: %s", len(m.humans), pastSevenDaysSignupsCount, m.revenue)) // Status bar showing current position statusBar := statusStyle.Render(fmt.Sprintf(" %d/%d", m.cursor+1, len(m.humans))) // Help text help := helpStyle.Render("↑/k up • ↓/j down • ctrl+u/d scroll detail • r refresh • q quit") // Create two-column layout leftPanel := m.viewport.View() rightPanel := m.detailViewport.View() // Combine panels side by side mainContent := lipgloss.JoinHorizontal( lipgloss.Top, leftPanel, " ", // spacing rightPanel, ) return fmt.Sprintf("%s%s\n%s\n%s", header, statusBar, mainContent, help) } func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { md := metadata.Pairs("authorization", "talksik") ctx = metadata.NewOutgoingContext(ctx, md) return invoker(ctx, method, req, reply, cc, opts...) } func main() { logrus.Infof("hello world") humansData := getHumansData() p := tea.NewProgram(initialModel(humansData)) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } } func getHumansData() []*HumanData { heliosAddr := "helios.flowy.live:443" creds := credentials.NewTLS(&tls.Config{ InsecureSkipVerify: true, }) heliosService, err := grpc.NewClient(heliosAddr, grpc.WithTransportCredentials(creds), grpc.WithUnaryInterceptor(unaryInterceptor)) if err != nil { logrus.Fatalf("connection to aero server invalid: %v", err) } defer heliosService.Close() humanService := pbhuman.NewHumanServiceClient(heliosService) keypadService := pbkeypad.NewKeypadServiceClient(heliosService) humansResponse, err := humanService.ListHumans(context.Background(), &pbhuman.ListHumansRequest{}) if err != nil { logrus.Fatalf("unable to fetch humans: %v", err) os.Exit(1) } keypadsResponse, err := keypadService.ListKeypads(context.Background(), &pbkeypad.ListKeypadsRequest{}) if err != nil { logrus.Fatalf("unable to fetch keypads: %v", err) os.Exit(1) } humansData := []*HumanData{} for _, human := range humansResponse.GetHumans() { keypadConfiguration := "" for _, keypad := range keypadsResponse.GetHumanKeypads() { if keypad.GetHumanId() == human.GetId() { keypadConfiguration = keypad.GetYamlConfig() } } humansData = append(humansData, &HumanData{ Email: human.GetEmail(), DisplayName: human.GetDisplayName(), JoinedAt: human.GetJoinedAt().AsTime(), KeypadConfiguration: keypadConfiguration, }) } return humansData }