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>
146 lines
4.2 KiB
Go
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
|
|
}
|