|
|
@@ -3,7 +3,9 @@ package main
|
|
|
|
import (
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/tls"
|
|
|
|
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
"time"
|
|
|
@@ -18,8 +20,170 @@ import (
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"google.golang.org/grpc/credentials"
|
|
|
|
"google.golang.org/grpc/credentials"
|
|
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
"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"`
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type SubscriptionResponse struct {
|
|
|
|
|
|
|
|
Items []struct {
|
|
|
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
|
|
|
GivesAccess bool `json:"gives_access"`
|
|
|
|
|
|
|
|
Entitlements struct {
|
|
|
|
|
|
|
|
Items []struct {
|
|
|
|
|
|
|
|
LookupKey string `json:"lookup_key"`
|
|
|
|
|
|
|
|
} `json:"items"`
|
|
|
|
|
|
|
|
} `json:"entitlements"`
|
|
|
|
|
|
|
|
} `json:"items"`
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func hasProSubscription(customerEmail string) bool {
|
|
|
|
|
|
|
|
client := &http.Client{}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("https://api.revenuecat.com/v2/projects/proj5b2f7356/customers/%s/subscriptions", customerEmail)
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
logrus.Errorf("Error creating subscription request for %s: %v", customerEmail, err)
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add authorization header
|
|
|
|
|
|
|
|
req.Header.Add("Authorization", "Bearer sk_wxmjNCLtnDSUPmSXlbEWyuqkpTVTi")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
response, err := client.Do(req)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
logrus.Errorf("Error fetching subscriptions for %s: %v", customerEmail, err)
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If customer not found or no subscriptions, return false
|
|
|
|
|
|
|
|
if response.StatusCode != 200 {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
body, err := io.ReadAll(response.Body)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
logrus.Errorf("Error reading subscription response for %s: %v", customerEmail, err)
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var subResp SubscriptionResponse
|
|
|
|
|
|
|
|
err = json.Unmarshal(body, &subResp)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
logrus.Errorf("Error parsing subscription JSON for %s: %v", customerEmail, err)
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if any subscription is active and has Pro entitlement
|
|
|
|
|
|
|
|
for _, sub := range subResp.Items {
|
|
|
|
|
|
|
|
if sub.Status == "active" && sub.GivesAccess {
|
|
|
|
|
|
|
|
for _, ent := range sub.Entitlements.Items {
|
|
|
|
|
|
|
|
if ent.LookupKey == "Pro" {
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
var (
|
|
|
|
titleStyle = lipgloss.NewStyle().
|
|
|
|
titleStyle = lipgloss.NewStyle().
|
|
|
|
Bold(true).
|
|
|
|
Bold(true).
|
|
|
@@ -93,6 +257,7 @@ type HumanData struct {
|
|
|
|
DisplayName string
|
|
|
|
DisplayName string
|
|
|
|
JoinedAt time.Time
|
|
|
|
JoinedAt time.Time
|
|
|
|
KeypadConfiguration string
|
|
|
|
KeypadConfiguration string
|
|
|
|
|
|
|
|
HasProSubscription bool
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type model struct {
|
|
|
|
type model struct {
|
|
|
@@ -102,17 +267,17 @@ type model struct {
|
|
|
|
viewport viewport.Model
|
|
|
|
viewport viewport.Model
|
|
|
|
detailViewport viewport.Model
|
|
|
|
detailViewport viewport.Model
|
|
|
|
ready bool
|
|
|
|
ready bool
|
|
|
|
showKeypad bool
|
|
|
|
|
|
|
|
terminalWidth int
|
|
|
|
terminalWidth int
|
|
|
|
terminalHeight int
|
|
|
|
terminalHeight int
|
|
|
|
focusOnDetail bool // true = detail panel has focus, false = list has focus
|
|
|
|
focusOnDetail bool // true = detail panel has focus, false = list has focus
|
|
|
|
|
|
|
|
revenue string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func initialModel(humans []*HumanData) model {
|
|
|
|
func initialModel(humans []*HumanData) model {
|
|
|
|
return model{
|
|
|
|
return model{
|
|
|
|
humans: humans,
|
|
|
|
humans: humans,
|
|
|
|
cursor: 0,
|
|
|
|
cursor: 0,
|
|
|
|
showKeypad: false,
|
|
|
|
revenue: getRevenue(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -174,7 +339,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
|
m.viewport.SetContent(m.renderList())
|
|
|
|
m.viewport.SetContent(m.renderList())
|
|
|
|
m.detailViewport.SetContent(m.renderDetails())
|
|
|
|
m.detailViewport.SetContent(m.renderDetails())
|
|
|
|
m.detailViewport.GotoTop() // Reset detail scroll position
|
|
|
|
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":
|
|
|
|
case "down", "j":
|
|
|
|
if m.cursor < len(m.humans)-1 {
|
|
|
|
if m.cursor < len(m.humans)-1 {
|
|
|
@@ -182,6 +359,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
|
m.viewport.SetContent(m.renderList())
|
|
|
|
m.viewport.SetContent(m.renderList())
|
|
|
|
m.detailViewport.SetContent(m.renderDetails())
|
|
|
|
m.detailViewport.SetContent(m.renderDetails())
|
|
|
|
m.detailViewport.GotoTop() // Reset detail scroll position
|
|
|
|
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
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -238,9 +420,28 @@ func (m model) renderDetails() string {
|
|
|
|
// Joined At
|
|
|
|
// Joined At
|
|
|
|
b.WriteString(detailLabelStyle.Render("Joined: "))
|
|
|
|
b.WriteString(detailLabelStyle.Render("Joined: "))
|
|
|
|
timeAgo := timediff.TimeDiff(human.JoinedAt)
|
|
|
|
timeAgo := timediff.TimeDiff(human.JoinedAt)
|
|
|
|
joinedFormatted := human.JoinedAt.Format("Jan 2, 2006 at 3:04 PM")
|
|
|
|
|
|
|
|
|
|
|
|
// Convert to Pacific timezone
|
|
|
|
|
|
|
|
loc, err := time.LoadLocation("America/Los_Angeles")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
loc = time.UTC // fallback to UTC if there's an error
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
joinedPacific := human.JoinedAt.In(loc)
|
|
|
|
|
|
|
|
joinedFormatted := joinedPacific.Format("Jan 2, 2006 at 3:04 PM MST")
|
|
|
|
|
|
|
|
|
|
|
|
b.WriteString(detailValueStyle.Render(fmt.Sprintf("%s (%s)", joinedFormatted, timeAgo)) + "\n\n")
|
|
|
|
b.WriteString(detailValueStyle.Render(fmt.Sprintf("%s (%s)", joinedFormatted, timeAgo)) + "\n\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Pro Subscription Status
|
|
|
|
|
|
|
|
b.WriteString(detailLabelStyle.Render("Pro Subscription: "))
|
|
|
|
|
|
|
|
if human.HasProSubscription {
|
|
|
|
|
|
|
|
proStyle := lipgloss.NewStyle().
|
|
|
|
|
|
|
|
Foreground(lipgloss.Color("#00FF00")).
|
|
|
|
|
|
|
|
Bold(true)
|
|
|
|
|
|
|
|
b.WriteString(proStyle.Render("✓ Active") + "\n\n")
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
b.WriteString(dimItemStyle.Render("✗ None") + "\n\n")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Keypad Configuration
|
|
|
|
// Keypad Configuration
|
|
|
|
if human.KeypadConfiguration != "" {
|
|
|
|
if human.KeypadConfiguration != "" {
|
|
|
|
b.WriteString(detailLabelStyle.Render("Keypad Configuration:") + "\n")
|
|
|
|
b.WriteString(detailLabelStyle.Render("Keypad Configuration:") + "\n")
|
|
|
@@ -266,13 +467,13 @@ func (m model) View() string {
|
|
|
|
pastSevenDaysSignupsCount++
|
|
|
|
pastSevenDaysSignupsCount++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
header := titleStyle.Render(fmt.Sprintf("👥 Humans | %d total | %d in the past 7 days", len(m.humans), 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
|
|
|
|
// Status bar showing current position
|
|
|
|
statusBar := statusStyle.Render(fmt.Sprintf(" %d/%d", m.cursor+1, len(m.humans)))
|
|
|
|
statusBar := statusStyle.Render(fmt.Sprintf(" %d/%d", m.cursor+1, len(m.humans)))
|
|
|
|
|
|
|
|
|
|
|
|
// Help text
|
|
|
|
// Help text
|
|
|
|
help := helpStyle.Render("↑/k up • ↓/j down • ctrl+u/d scroll detail • q quit")
|
|
|
|
help := helpStyle.Render("↑/k up • ↓/j down • ctrl+u/d scroll detail • r refresh • q quit")
|
|
|
|
|
|
|
|
|
|
|
|
// Create two-column layout
|
|
|
|
// Create two-column layout
|
|
|
|
leftPanel := m.viewport.View()
|
|
|
|
leftPanel := m.viewport.View()
|
|
|
@@ -296,8 +497,17 @@ func unaryInterceptor(ctx context.Context, method string, req, reply interface{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
func main() {
|
|
|
|
logrus.Infof("hello world")
|
|
|
|
logrus.Infof("Loading customer data...wait a moment")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
heliosAddr := "helios.flowy.live:443"
|
|
|
|
creds := credentials.NewTLS(&tls.Config{
|
|
|
|
creds := credentials.NewTLS(&tls.Config{
|
|
|
|
InsecureSkipVerify: true,
|
|
|
|
InsecureSkipVerify: true,
|
|
|
@@ -322,12 +532,6 @@ func main() {
|
|
|
|
os.Exit(1)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
keypadsResponse.GetHumanKeypads()[0].GetHumanId()
|
|
|
|
|
|
|
|
keypadsResponse.GetHumanKeypads()[0].GetYamlConfig()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logrus.Info("connected to helios")
|
|
|
|
|
|
|
|
logrus.Infof("fetched %d humans", len(humansResponse.GetHumans()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
humansData := []*HumanData{}
|
|
|
|
humansData := []*HumanData{}
|
|
|
|
for _, human := range humansResponse.GetHumans() {
|
|
|
|
for _, human := range humansResponse.GetHumans() {
|
|
|
|
keypadConfiguration := ""
|
|
|
|
keypadConfiguration := ""
|
|
|
@@ -336,17 +540,18 @@ func main() {
|
|
|
|
keypadConfiguration = keypad.GetYamlConfig()
|
|
|
|
keypadConfiguration = keypad.GetYamlConfig()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if customer has pro subscription
|
|
|
|
|
|
|
|
hasPro := hasProSubscription(human.GetEmail())
|
|
|
|
|
|
|
|
|
|
|
|
humansData = append(humansData, &HumanData{
|
|
|
|
humansData = append(humansData, &HumanData{
|
|
|
|
Email: human.GetEmail(),
|
|
|
|
Email: human.GetEmail(),
|
|
|
|
DisplayName: human.GetDisplayName(),
|
|
|
|
DisplayName: human.GetDisplayName(),
|
|
|
|
JoinedAt: human.GetJoinedAt().AsTime(),
|
|
|
|
JoinedAt: human.GetJoinedAt().AsTime(),
|
|
|
|
KeypadConfiguration: keypadConfiguration,
|
|
|
|
KeypadConfiguration: keypadConfiguration,
|
|
|
|
|
|
|
|
HasProSubscription: hasPro,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
p := tea.NewProgram(initialModel(humansData))
|
|
|
|
return humansData
|
|
|
|
if _, err := p.Run(); err != nil {
|
|
|
|
|
|
|
|
fmt.Printf("Alas, there's been an error: %v", err)
|
|
|
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|