LEM/cmd/lem-desktop/main.go
Claude 774f097855
feat: scaffold LEM Desktop app (Wails v3 system tray + Docker stack)
Inspired by BugSETI architecture — system tray with WebView2 windows,
Docker Compose stack (Forgejo + InfluxDB + inference proxy), and
scoring agent integration. Builds as signed native binary on macOS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 17:43:19 +00:00

146 lines
4.2 KiB
Go

// Package main provides the LEM Desktop application.
// A system tray app inspired by BugSETI that bundles:
// - Local Forgejo for agentic git workflows
// - InfluxDB for metrics and coordination
// - Inference proxy to M3 MLX or local vLLM
// - Scoring agent for automated checkpoint evaluation
// - Lab dashboard for training and generation monitoring
//
// Built on Wails v3 — ships as a signed native binary on macOS (Lethean CIC),
// Linux AppImage, and Windows installer.
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"forge.lthn.ai/lthn/lem/cmd/lem-desktop/icons"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
//go:embed all:frontend
var assets embed.FS
// Tray icon data — placeholders until real icons are generated.
var (
trayIconTemplate = icons.Placeholder()
trayIconLight = icons.Placeholder()
trayIconDark = icons.Placeholder()
)
func main() {
// Strip embed prefix so files serve from root.
staticAssets, err := fs.Sub(assets, "frontend")
if err != nil {
log.Fatal(err)
}
// ── Configuration ──
influxURL := envOr("INFLUX_URL", "http://localhost:8181")
influxDB := envOr("INFLUX_DB", "training")
apiURL := envOr("LEM_API_URL", "http://localhost:8080")
m3Host := envOr("M3_HOST", "10.69.69.108")
baseModel := envOr("BASE_MODEL", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B")
dbPath := envOr("LEM_DB", "")
workDir := envOr("WORK_DIR", filepath.Join(os.TempDir(), "scoring-agent"))
deployDir := envOr("LEM_DEPLOY_DIR", findDeployDir())
// ── Services ──
dashboardService := NewDashboardService(influxURL, influxDB, dbPath)
dockerService := NewDockerService(deployDir)
agentRunner := NewAgentRunner(apiURL, influxURL, influxDB, m3Host, baseModel, workDir)
trayService := NewTrayService(nil)
services := []application.Service{
application.NewService(dashboardService),
application.NewService(dockerService),
application.NewService(agentRunner),
application.NewService(trayService),
}
// ── Application ──
app := application.New(application.Options{
Name: "LEM",
Description: "Lethean Ethics Model — Training, Scoring & Inference",
Services: services,
Assets: application.AssetOptions{
Handler: spaHandler(staticAssets),
},
Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory,
},
})
// Wire up references.
trayService.app = app
trayService.SetServices(dashboardService, dockerService, agentRunner)
// Set up system tray.
setupSystemTray(app, trayService, dashboardService, dockerService)
// Show dashboard on first launch.
app.Event.RegisterApplicationEventHook(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
if w, ok := app.Window.Get("dashboard"); ok {
w.Show()
w.Focus()
}
})
log.Println("Starting LEM Desktop...")
log.Println(" - System tray active")
log.Println(" - Dashboard ready")
log.Printf(" - InfluxDB: %s/%s", influxURL, influxDB)
log.Printf(" - Inference: %s", apiURL)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
// spaHandler serves static files with SPA fallback for client-side routing.
func spaHandler(fsys fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
if path == "" {
path = "index.html"
}
if _, err := fs.Stat(fsys, path); err != nil {
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
// findDeployDir locates the deploy/ directory relative to the binary.
func findDeployDir() string {
// Check relative to executable.
exe, err := os.Executable()
if err == nil {
dir := filepath.Join(filepath.Dir(exe), "deploy")
if _, err := os.Stat(filepath.Join(dir, "docker-compose.yml")); err == nil {
return dir
}
}
// Check relative to working directory.
if cwd, err := os.Getwd(); err == nil {
dir := filepath.Join(cwd, "deploy")
if _, err := os.Stat(filepath.Join(dir, "docker-compose.yml")); err == nil {
return dir
}
}
return "deploy"
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}