package main import ( "context" "fmt" "net/http" "github.com/wailsapp/wails/v2/pkg/runtime" "rickub.com/bots-garden/ori-desktop/internal/health" "rickub.com/bots-garden/ori-desktop/internal/settings" ) // App holds the state shared by the Go-bound methods exposed to the frontend. // Every exported method on App is callable from JavaScript as // window.go.main.App.(...) and returns a Promise. type App struct { ctx context.Context settingsPath string client *http.Client } // NewApp creates the application state. The settings location is resolved // once here; if the platform exposes no user config directory the path is // left empty and SaveConfig reports the problem instead of guessing. func NewApp() *App { path, err := settings.DefaultPath() if err != nil { path = "" } return &App{ settingsPath: path, client: &http.Client{Timeout: health.DefaultTimeout}, } } // startup is the Wails OnStartup hook; it keeps the runtime context needed by // runtime.* calls such as BrowserOpenURL. func (a *App) startup(ctx context.Context) { a.ctx = ctx } // GetConfig returns the remembered settings (defaults on a first run). func (a *App) GetConfig() (settings.Settings, error) { if a.settingsPath == "" { return settings.Default(), nil } return settings.Load(a.settingsPath) } // SaveConfig normalises the server URL and persists the settings. func (a *App) SaveConfig(s settings.Settings) error { if a.settingsPath == "" { return fmt.Errorf("no user configuration directory available on this platform") } normalized, err := settings.NormalizeURL(s.ServerURL) if err != nil { return err } s.ServerURL = normalized return settings.Save(a.settingsPath, s) } // CheckHealth probes GET /healthz and reports the outcome. It never // fails the Promise: a malformed URL or an unreachable server both come back // as a Report with OK=false and an explanatory Detail. func (a *App) CheckHealth(rawURL string) health.Report { base, err := settings.NormalizeURL(rawURL) if err != nil { return health.Report{URL: rawURL, Detail: err.Error()} } return health.Check(a.context(), a.client, base) } // Connect is the one-call happy path used by the connection screen: it // normalises the URL, checks /healthz, remembers the URL on success and // returns the normalised base URL the frontend should load. On failure the // Promise rejects with the health Detail so the screen can show it. func (a *App) Connect(rawURL string) (string, error) { base, err := settings.NormalizeURL(rawURL) if err != nil { return "", err } rep := health.Check(a.context(), a.client, base) if !rep.OK { return "", fmt.Errorf("%s", rep.Detail) } if a.settingsPath != "" { if err := settings.Save(a.settingsPath, settings.Settings{ServerURL: base}); err != nil { // Not fatal for the session: the user is connected, only the // memory of the URL is lost. Surface it in the returned error? // No — the frontend treats an error as "not connected"; log it. fmt.Println("ori-desktop: warning:", err) } } return base, nil } // OpenInBrowser opens the given URL (normalised first) in the system's // default browser — the fallback when embedding is not wanted. func (a *App) OpenInBrowser(rawURL string) error { base, err := settings.NormalizeURL(rawURL) if err != nil { return err } if a.ctx == nil { return fmt.Errorf("application runtime not ready") } runtime.BrowserOpenURL(a.ctx, base) return nil } // SettingsPath tells the frontend where the settings live, for display. func (a *App) SettingsPath() string { return a.settingsPath } // context returns the runtime context, or a background one before startup // (bound methods are only reachable after startup, this is belt and braces). func (a *App) context() context.Context { if a.ctx != nil { return a.ctx } return context.Background() }