190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
// Package main implements the ML TUI
|
|
package main
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/jfraeys/fetch_ml/cmd/tui/internal/config"
|
|
"github.com/jfraeys/fetch_ml/cmd/tui/internal/controller"
|
|
"github.com/jfraeys/fetch_ml/cmd/tui/internal/model"
|
|
"github.com/jfraeys/fetch_ml/cmd/tui/internal/services"
|
|
"github.com/jfraeys/fetch_ml/cmd/tui/internal/view"
|
|
"github.com/jfraeys/fetch_ml/internal/auth"
|
|
"github.com/jfraeys/fetch_ml/internal/logging"
|
|
)
|
|
|
|
// AppModel represents the main application model for the TUI.
|
|
type AppModel struct {
|
|
state model.State
|
|
controller *controller.Controller
|
|
}
|
|
|
|
// Init initializes the TUI application.
|
|
func (m AppModel) Init() tea.Cmd {
|
|
return m.controller.Init()
|
|
}
|
|
|
|
// Update handles application updates and messages.
|
|
func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
newState, cmd := m.controller.Update(msg, m.state)
|
|
m.state = newState
|
|
return m, cmd
|
|
}
|
|
|
|
// View renders the TUI interface.
|
|
func (m AppModel) View() string {
|
|
return view.Render(m.state)
|
|
}
|
|
|
|
func main() {
|
|
// Parse authentication flags
|
|
authFlags := auth.ParseAuthFlags()
|
|
if err := auth.ValidateFlags(authFlags); err != nil {
|
|
log.Fatalf("Authentication flag error: %v", err)
|
|
}
|
|
|
|
// Get API key from various sources
|
|
apiKey := auth.GetAPIKeyFromSources(authFlags)
|
|
|
|
var (
|
|
cfg *config.Config
|
|
cliConfig *config.CLIConfig
|
|
cliConfPath string
|
|
)
|
|
|
|
configFlag := authFlags.ConfigFile
|
|
|
|
// Only support TOML configuration
|
|
var err error
|
|
cliConfig, cliConfPath, err = config.LoadCLIConfig(configFlag)
|
|
if err != nil {
|
|
if configFlag != "" {
|
|
log.Fatalf("Failed to load TOML config %s: %v", configFlag, err)
|
|
}
|
|
// Provide helpful error message for data scientists
|
|
log.Printf("=== Fetch ML TUI - Configuration Required ===")
|
|
log.Printf("")
|
|
log.Printf("Error: %v", err)
|
|
log.Printf("")
|
|
log.Printf("To get started with the TUI, you need to initialize your configuration:")
|
|
log.Printf("")
|
|
log.Printf("Option 1: Using the Zig CLI (Recommended)")
|
|
log.Printf(" 1. Build the CLI: cd cli && make build")
|
|
log.Printf(" 2. Initialize config: ./cli/zig-out/bin/ml init")
|
|
log.Printf(" 3. Edit ~/.ml/config.toml with your settings")
|
|
log.Printf(" 4. Run TUI: ./bin/tui")
|
|
log.Printf("")
|
|
log.Printf("Option 2: Manual Configuration")
|
|
log.Printf(" 1. Create directory: mkdir -p ~/.ml")
|
|
log.Printf(" 2. Create config: touch ~/.ml/config.toml")
|
|
log.Printf(" 3. Add your settings to the file")
|
|
log.Printf(" 4. Run TUI: ./bin/tui")
|
|
log.Printf("")
|
|
log.Printf("Example ~/.ml/config.toml:")
|
|
log.Printf(" worker_host = \"localhost\"")
|
|
log.Printf(" worker_user = \"your_username\"")
|
|
log.Printf(" worker_base = \"~/ml_jobs\"")
|
|
log.Printf(" worker_port = 22")
|
|
log.Printf(" api_key = \"your_api_key_here\"")
|
|
log.Printf("")
|
|
log.Printf("For more help, see: https://github.com/jfraeys/fetch_ml/docs")
|
|
os.Exit(1)
|
|
}
|
|
|
|
cfg, err = cliConfig.ToTUIConfig()
|
|
if err != nil {
|
|
log.Fatalf("Failed to convert CLI config to TUI config: %v", err)
|
|
}
|
|
log.Printf("Loaded TOML configuration from %s", cliConfPath)
|
|
|
|
// Validate authentication configuration
|
|
if err := cfg.Auth.ValidateAuthConfig(); err != nil {
|
|
log.Fatalf("Invalid authentication configuration: %v", err)
|
|
}
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
log.Fatalf("Invalid configuration: %v", err)
|
|
}
|
|
|
|
// Test authentication if enabled
|
|
if cfg.Auth.Enabled {
|
|
// Use API key from CLI config if available, otherwise use from flags
|
|
var effectiveAPIKey string
|
|
switch {
|
|
case cliConfig != nil && cliConfig.APIKey != "":
|
|
effectiveAPIKey = cliConfig.APIKey
|
|
case apiKey != "":
|
|
effectiveAPIKey = apiKey
|
|
default:
|
|
log.Fatal("Authentication required but no API key provided")
|
|
}
|
|
|
|
if _, err := cfg.Auth.ValidateAPIKey(effectiveAPIKey); err != nil {
|
|
log.Fatalf("Authentication failed: %v", err)
|
|
}
|
|
}
|
|
|
|
srv, err := services.NewMLServer(cfg)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to server: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := srv.Close(); err != nil {
|
|
log.Printf("server close error: %v", err)
|
|
}
|
|
}()
|
|
|
|
tq, err := services.NewTaskQueue(cfg)
|
|
if err != nil {
|
|
log.Printf("Failed to connect to Redis: %v", err)
|
|
return
|
|
}
|
|
defer func() {
|
|
if err := tq.Close(); err != nil {
|
|
log.Printf("task queue close error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Initialize logger with error level and no debug output
|
|
logger := logging.NewLogger(-4, false) // -4 = slog.LevelError
|
|
|
|
// Initialize State and Controller
|
|
var effectiveAPIKey string
|
|
if cliConfig != nil && cliConfig.APIKey != "" {
|
|
effectiveAPIKey = cliConfig.APIKey
|
|
} else {
|
|
effectiveAPIKey = apiKey
|
|
}
|
|
|
|
initialState := model.InitialState(effectiveAPIKey)
|
|
ctrl := controller.New(cfg, srv, tq, logger)
|
|
|
|
appModel := AppModel{
|
|
state: initialState,
|
|
controller: ctrl,
|
|
}
|
|
|
|
// Run TUI program with graceful shutdown on SIGINT/SIGTERM
|
|
p := tea.NewProgram(appModel, tea.WithAltScreen(), tea.WithMouseAllMotion())
|
|
|
|
// Set up signal handling for graceful shutdown
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
<-sigChan
|
|
p.Quit()
|
|
}()
|
|
|
|
if _, err := p.Run(); err != nil {
|
|
_ = p.ReleaseTerminal()
|
|
log.Printf("Error running TUI: %v", err)
|
|
return
|
|
}
|
|
|
|
_ = p.ReleaseTerminal()
|
|
}
|