feat: replace web GUI with native Windows system tray via syscall
- webgui.go: Native Windows tray icon + right-click popup menu using pure syscall (Shell_NotifyIconW, CreatePopupMenu, etc.) - No CGO required, no external dependencies - Shows server status, Start/Stop, Open Dashboard, Quit - Hides console window on startup - Handles OS signals and Windows session end - webgui_other.go: Stub for non-Windows notray builds - Remove webgui_html.go (browser-based approach no longer needed) - Fix auto-start: replace schtasks (fails on Chinese Windows) with Registry Run key (HKCU\...\Run) which works without admin and has no locale encoding issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
db6388523f
commit
74a850a00c
@ -129,31 +129,40 @@ func removeQuarantine(installDir string) {
|
||||
}
|
||||
|
||||
func installAutoRestart(installDir string) error {
|
||||
taskName := "EdgeAIPlatformServer"
|
||||
binPath := filepath.Join(installDir, "edge-ai-server.exe")
|
||||
regValue := fmt.Sprintf(`"%s" --gui`, binPath)
|
||||
|
||||
// Remove existing task if present
|
||||
exec.Command("schtasks", "/Delete", "/TN", taskName, "/F").Run()
|
||||
|
||||
// Use schtasks instead of PowerShell Register-ScheduledTask
|
||||
// to avoid XML parsing issues on non-English Windows
|
||||
// Note: do not use /RL HIGHEST — it requires admin privileges
|
||||
cmd := exec.Command("schtasks", "/Create",
|
||||
"/TN", taskName,
|
||||
"/TR", fmt.Sprintf(`"%s" --gui`, binPath),
|
||||
"/SC", "ONLOGON",
|
||||
"/F",
|
||||
// Use Registry Run key for auto-start (no admin required, works on all Windows locales)
|
||||
cmd := exec.Command("reg", "add",
|
||||
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`,
|
||||
"/v", "EdgeAIPlatformServer",
|
||||
"/t", "REG_SZ",
|
||||
"/d", regValue,
|
||||
"/f",
|
||||
)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("scheduled task creation failed: %s — %w", string(out), err)
|
||||
return fmt.Errorf("registry auto-start setup failed: %s — %w", string(out), err)
|
||||
}
|
||||
|
||||
// Start the task immediately
|
||||
exec.Command("schtasks", "/Run", "/TN", taskName).Run()
|
||||
// Also clean up any old scheduled task
|
||||
exec.Command("schtasks", "/Delete", "/TN", "EdgeAIPlatformServer", "/F").Run()
|
||||
|
||||
// Start the server immediately
|
||||
startCmd := exec.Command(binPath, "--gui")
|
||||
startCmd.Dir = installDir
|
||||
startCmd.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAutoRestart() {
|
||||
// Remove Registry Run key
|
||||
exec.Command("reg", "delete",
|
||||
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`,
|
||||
"/v", "EdgeAIPlatformServer",
|
||||
"/f",
|
||||
).Run()
|
||||
|
||||
// Also clean up any old scheduled task
|
||||
exec.Command("schtasks", "/Delete", "/TN", "EdgeAIPlatformServer", "/F").Run()
|
||||
}
|
||||
|
||||
@ -1,136 +1,332 @@
|
||||
//go:build notray
|
||||
//go:build notray && windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// webGUIApp manages the web-based GUI controller and server child process.
|
||||
type webGUIApp struct {
|
||||
// Windows API constants
|
||||
const (
|
||||
wmApp = 0x8000
|
||||
wmTrayIcon = wmApp + 1
|
||||
wmCommand = 0x0111
|
||||
wmDestroy = 0x0002
|
||||
wmLButtonUp = 0x0202
|
||||
wmRButtonUp = 0x0205
|
||||
wmContextMenu = 0x007B
|
||||
wmClose = 0x0010
|
||||
wmQueryEndSess = 0x0011
|
||||
|
||||
nimAdd = 0x00000000
|
||||
nimModify = 0x00000001
|
||||
nimDelete = 0x00000002
|
||||
|
||||
nifMessage = 0x00000001
|
||||
nifIcon = 0x00000002
|
||||
nifTip = 0x00000004
|
||||
|
||||
idmToggle = 1001
|
||||
idmDashboard = 1002
|
||||
idmQuit = 1003
|
||||
|
||||
mfString = 0x00000000
|
||||
mfSeparator = 0x00000800
|
||||
mfEnabled = 0x00000000
|
||||
mfGrayed = 0x00000001
|
||||
|
||||
tpmRightAlign = 0x0008
|
||||
tpmBottomAlign = 0x0020
|
||||
|
||||
imageIcon = 1
|
||||
lrShared = 0x00008000
|
||||
idiApp = 32512 // default application icon
|
||||
|
||||
swHide = 0
|
||||
)
|
||||
|
||||
// NOTIFYICONDATAW for Shell_NotifyIconW
|
||||
type notifyIconData struct {
|
||||
CbSize uint32
|
||||
HWnd uintptr
|
||||
UID uint32
|
||||
UFlags uint32
|
||||
UCallbackMessage uint32
|
||||
HIcon uintptr
|
||||
SzTip [128]uint16
|
||||
}
|
||||
|
||||
var (
|
||||
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procShellNotifyIcon = shell32.NewProc("Shell_NotifyIconW")
|
||||
procRegisterClassEx = user32.NewProc("RegisterClassExW")
|
||||
procCreateWindowEx = user32.NewProc("CreateWindowExW")
|
||||
procDefWindowProc = user32.NewProc("DefWindowProcW")
|
||||
procGetMessage = user32.NewProc("GetMessageW")
|
||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||
procDispatchMessage = user32.NewProc("DispatchMessageW")
|
||||
procPostQuitMessage = user32.NewProc("PostQuitMessage")
|
||||
procCreatePopupMenu = user32.NewProc("CreatePopupMenu")
|
||||
procAppendMenu = user32.NewProc("AppendMenuW")
|
||||
procTrackPopupMenu = user32.NewProc("TrackPopupMenu")
|
||||
procDestroyMenu = user32.NewProc("DestroyMenu")
|
||||
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
||||
procPostMessage = user32.NewProc("PostMessageW")
|
||||
procGetCursorPos = user32.NewProc("GetCursorPos")
|
||||
procLoadIcon = user32.NewProc("LoadIconW")
|
||||
procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
|
||||
procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
)
|
||||
|
||||
type point struct {
|
||||
X, Y int32
|
||||
}
|
||||
|
||||
type wndClassEx struct {
|
||||
Size uint32
|
||||
Style uint32
|
||||
WndProc uintptr
|
||||
ClsExtra int32
|
||||
WndExtra int32
|
||||
Instance uintptr
|
||||
Icon uintptr
|
||||
Cursor uintptr
|
||||
Background uintptr
|
||||
MenuName *uint16
|
||||
ClassName *uint16
|
||||
IconSm uintptr
|
||||
}
|
||||
|
||||
type msg struct {
|
||||
HWnd uintptr
|
||||
Message uint32
|
||||
WParam uintptr
|
||||
LParam uintptr
|
||||
Time uint32
|
||||
Pt point
|
||||
}
|
||||
|
||||
// global state for the wndProc callback
|
||||
var globalApp *nativeTrayApp
|
||||
|
||||
type nativeTrayApp struct {
|
||||
mu sync.Mutex
|
||||
cmd *exec.Cmd
|
||||
running bool
|
||||
cfg *Config
|
||||
hwnd uintptr
|
||||
nid notifyIconData
|
||||
hIcon uintptr
|
||||
}
|
||||
|
||||
// RunWebGUI starts a lightweight HTTP server that serves a browser-based
|
||||
// control panel for managing the Edge AI server process. This is the
|
||||
// CGO-free alternative to the systray-based Run() function.
|
||||
// RunWebGUI starts a native Windows system tray application.
|
||||
func RunWebGUI(cfg *Config) {
|
||||
app := &webGUIApp{cfg: cfg}
|
||||
app := &nativeTrayApp{cfg: cfg}
|
||||
globalApp = app
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", app.handleIndex)
|
||||
mux.HandleFunc("/api/status", app.handleStatus)
|
||||
mux.HandleFunc("/api/start", app.handleStart)
|
||||
mux.HandleFunc("/api/stop", app.handleStop)
|
||||
mux.HandleFunc("/api/open-dashboard", app.handleOpenDashboard)
|
||||
mux.HandleFunc("/api/quit", app.handleQuit)
|
||||
|
||||
// Listen on a random available port
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
log.Fatalf("[gui] failed to listen: %v", err)
|
||||
// Hide console window
|
||||
if hwndConsole, _, _ := procGetConsoleWindow.Call(); hwndConsole != 0 {
|
||||
procShowWindow.Call(hwndConsole, swHide)
|
||||
}
|
||||
guiAddr := ln.Addr().String()
|
||||
log.Printf("[gui] Control panel: http://%s", guiAddr)
|
||||
|
||||
// Get module handle
|
||||
hInstance, _, _ := procGetModuleHandle.Call(0)
|
||||
|
||||
// Load default application icon
|
||||
app.hIcon, _, _ = procLoadIcon.Call(0, uintptr(idiApp))
|
||||
|
||||
// Register window class
|
||||
className := syscall.StringToUTF16Ptr("EdgeAITrayClass")
|
||||
wcx := wndClassEx{
|
||||
Size: uint32(unsafe.Sizeof(wndClassEx{})),
|
||||
WndProc: syscall.NewCallback(wndProc),
|
||||
Instance: hInstance,
|
||||
ClassName: className,
|
||||
}
|
||||
procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wcx)))
|
||||
|
||||
// Create message-only window
|
||||
hwndMessageOnly := uintptr(0xFFFFFFFFFFFFFFFD) // HWND_MESSAGE = -3
|
||||
app.hwnd, _, _ = procCreateWindowEx.Call(
|
||||
0,
|
||||
uintptr(unsafe.Pointer(className)),
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Edge AI Platform"))),
|
||||
0, 0, 0, 0, 0,
|
||||
hwndMessageOnly, 0, hInstance, 0,
|
||||
)
|
||||
|
||||
// Set up notification icon
|
||||
app.nid = notifyIconData{
|
||||
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||
HWnd: app.hwnd,
|
||||
UID: 1,
|
||||
UFlags: nifMessage | nifIcon | nifTip,
|
||||
UCallbackMessage: wmTrayIcon,
|
||||
HIcon: app.hIcon,
|
||||
}
|
||||
copy(app.nid.SzTip[:], syscall.StringToUTF16("Edge AI Platform - Starting..."))
|
||||
procShellNotifyIcon.Call(nimAdd, uintptr(unsafe.Pointer(&app.nid)))
|
||||
|
||||
// Auto-start the server
|
||||
app.startServer()
|
||||
app.updateTrayTip()
|
||||
|
||||
// Open browser to the control panel
|
||||
openBrowserURL(fmt.Sprintf("http://%s", guiAddr))
|
||||
|
||||
// Handle OS signals for graceful shutdown
|
||||
// Handle OS signals
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-quit
|
||||
log.Println("[gui] Shutting down...")
|
||||
app.stopServer()
|
||||
app.cleanup()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Serve (blocks)
|
||||
if err := http.Serve(ln, mux); err != nil {
|
||||
log.Fatalf("[gui] server error: %v", err)
|
||||
log.Printf("[gui] System tray started, server on port %d", cfg.Server.Port)
|
||||
|
||||
// Windows message loop
|
||||
var m msg
|
||||
for {
|
||||
ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
|
||||
if ret == 0 { // WM_QUIT
|
||||
break
|
||||
}
|
||||
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||||
procDispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||||
}
|
||||
|
||||
app.cleanup()
|
||||
}
|
||||
|
||||
func (a *webGUIApp) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(w, webGUIHTML)
|
||||
func wndProc(hwnd uintptr, umsg uint32, wParam, lParam uintptr) uintptr {
|
||||
app := globalApp
|
||||
if app == nil {
|
||||
ret, _, _ := procDefWindowProc.Call(hwnd, uintptr(umsg), wParam, lParam)
|
||||
return ret
|
||||
}
|
||||
|
||||
switch umsg {
|
||||
case wmTrayIcon:
|
||||
switch lParam {
|
||||
case wmLButtonUp, wmRButtonUp:
|
||||
app.showMenu()
|
||||
}
|
||||
return 0
|
||||
|
||||
case wmCommand:
|
||||
cmdID := int(wParam & 0xFFFF)
|
||||
switch cmdID {
|
||||
case idmToggle:
|
||||
app.mu.Lock()
|
||||
running := app.running
|
||||
app.mu.Unlock()
|
||||
if running {
|
||||
app.stopServer()
|
||||
} else {
|
||||
app.startServer()
|
||||
}
|
||||
app.updateTrayTip()
|
||||
case idmDashboard:
|
||||
app.openDashboard()
|
||||
case idmQuit:
|
||||
app.stopServer()
|
||||
procShellNotifyIcon.Call(nimDelete, uintptr(unsafe.Pointer(&app.nid)))
|
||||
procPostQuitMessage.Call(0)
|
||||
}
|
||||
return 0
|
||||
|
||||
case wmClose, wmDestroy:
|
||||
app.cleanup()
|
||||
procPostQuitMessage.Call(0)
|
||||
return 0
|
||||
|
||||
case wmQueryEndSess:
|
||||
app.stopServer()
|
||||
return 1 // allow session end
|
||||
}
|
||||
|
||||
ret, _, _ := procDefWindowProc.Call(hwnd, uintptr(umsg), wParam, lParam)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (a *webGUIApp) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *nativeTrayApp) showMenu() {
|
||||
hMenu, _, _ := procCreatePopupMenu.Call()
|
||||
|
||||
a.mu.Lock()
|
||||
running := a.running
|
||||
a.mu.Unlock()
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"running": running,
|
||||
"port": a.cfg.Server.Port,
|
||||
"host": a.cfg.Server.Host,
|
||||
// Status line (disabled)
|
||||
var statusText string
|
||||
if running {
|
||||
statusText = fmt.Sprintf("Status: Running (:%d)", a.cfg.Server.Port)
|
||||
} else {
|
||||
statusText = "Status: Stopped"
|
||||
}
|
||||
if a.cfg.Relay.URL != "" {
|
||||
resp["relay"] = a.cfg.Relay.URL
|
||||
procAppendMenu.Call(hMenu, uintptr(mfString|mfGrayed), 0,
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(statusText))))
|
||||
|
||||
// Separator
|
||||
procAppendMenu.Call(hMenu, uintptr(mfSeparator), 0, 0)
|
||||
|
||||
// Toggle button
|
||||
var toggleText string
|
||||
if running {
|
||||
toggleText = "Stop Server"
|
||||
} else {
|
||||
toggleText = "Start Server"
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
procAppendMenu.Call(hMenu, uintptr(mfString|mfEnabled), uintptr(idmToggle),
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(toggleText))))
|
||||
|
||||
// Open Dashboard (grayed if not running)
|
||||
dashFlags := mfString | mfEnabled
|
||||
if !running {
|
||||
dashFlags = mfString | mfGrayed
|
||||
}
|
||||
procAppendMenu.Call(hMenu, uintptr(dashFlags), uintptr(idmDashboard),
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Open Dashboard"))))
|
||||
|
||||
// Separator + Quit
|
||||
procAppendMenu.Call(hMenu, uintptr(mfSeparator), 0, 0)
|
||||
procAppendMenu.Call(hMenu, uintptr(mfString|mfEnabled), uintptr(idmQuit),
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Quit"))))
|
||||
|
||||
// Get cursor position and show menu
|
||||
var pt point
|
||||
procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
||||
procSetForegroundWindow.Call(a.hwnd)
|
||||
procTrackPopupMenu.Call(hMenu, 0, uintptr(pt.X), uintptr(pt.Y), 0, a.hwnd, 0)
|
||||
procDestroyMenu.Call(hMenu)
|
||||
|
||||
// Post empty message to dismiss menu properly
|
||||
procPostMessage.Call(a.hwnd, 0, 0, 0)
|
||||
}
|
||||
|
||||
func (a *webGUIApp) handleStart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
func (a *nativeTrayApp) updateTrayTip() {
|
||||
a.mu.Lock()
|
||||
running := a.running
|
||||
a.mu.Unlock()
|
||||
|
||||
var tip string
|
||||
if running {
|
||||
tip = fmt.Sprintf("Edge AI Platform - Running (:%d)", a.cfg.Server.Port)
|
||||
} else {
|
||||
tip = "Edge AI Platform - Stopped"
|
||||
}
|
||||
a.startServer()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
copy(a.nid.SzTip[:], syscall.StringToUTF16(tip))
|
||||
procShellNotifyIcon.Call(nimModify, uintptr(unsafe.Pointer(&a.nid)))
|
||||
}
|
||||
|
||||
func (a *webGUIApp) handleStop(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
a.stopServer()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *webGUIApp) handleOpenDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
a.openDashboard()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *webGUIApp) handleQuit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
a.stopServer()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *webGUIApp) startServer() {
|
||||
func (a *nativeTrayApp) startServer() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
@ -172,6 +368,8 @@ func (a *webGUIApp) startServer() {
|
||||
a.mu.Lock()
|
||||
a.running = false
|
||||
a.mu.Unlock()
|
||||
// Update tray tooltip from the message loop thread
|
||||
a.updateTrayTip()
|
||||
if err != nil {
|
||||
log.Printf("[gui] server process exited: %v", err)
|
||||
} else {
|
||||
@ -180,7 +378,7 @@ func (a *webGUIApp) startServer() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *webGUIApp) stopServer() {
|
||||
func (a *nativeTrayApp) stopServer() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
@ -188,21 +386,17 @@ func (a *webGUIApp) stopServer() {
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
a.cmd.Process.Kill()
|
||||
} else {
|
||||
a.cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
a.cmd.Process.Kill()
|
||||
log.Println("[gui] Server stop requested")
|
||||
}
|
||||
|
||||
func (a *webGUIApp) openDashboard() {
|
||||
func (a *nativeTrayApp) openDashboard() {
|
||||
var url string
|
||||
if a.cfg.Relay.URL != "" && a.cfg.Relay.Token != "" {
|
||||
u := a.cfg.Relay.URL
|
||||
u = strings.Replace(u, "wss://", "https://", 1)
|
||||
u = strings.Replace(u, "ws://", "http://", 1)
|
||||
if i := strings.Index(u, "/tunnel"); i != -1 {
|
||||
u = replacePrefix(u, "wss://", "https://")
|
||||
u = replacePrefix(u, "ws://", "http://")
|
||||
if i := indexOf(u, "/tunnel"); i != -1 {
|
||||
u = u[:i]
|
||||
}
|
||||
url = fmt.Sprintf("%s/?token=%s", u, a.cfg.Relay.Token)
|
||||
@ -213,22 +407,29 @@ func (a *webGUIApp) openDashboard() {
|
||||
}
|
||||
url = fmt.Sprintf("http://%s:%d", host, a.cfg.Server.Port)
|
||||
}
|
||||
openBrowserURL(url)
|
||||
|
||||
exec.Command("cmd", "/c", "start", url).Start()
|
||||
}
|
||||
|
||||
func openBrowserURL(url string) {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", url)
|
||||
case "linux":
|
||||
cmd = exec.Command("xdg-open", url)
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", "start", url)
|
||||
default:
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("[gui] failed to open browser: %v", err)
|
||||
}
|
||||
func (a *nativeTrayApp) cleanup() {
|
||||
a.stopServer()
|
||||
procShellNotifyIcon.Call(nimDelete, uintptr(unsafe.Pointer(&a.nid)))
|
||||
}
|
||||
|
||||
// replacePrefix replaces old prefix with new if present.
|
||||
func replacePrefix(s, old, new string) string {
|
||||
if len(s) >= len(old) && s[:len(old)] == old {
|
||||
return new + s[len(old):]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// indexOf returns the index of substr in s, or -1 if not found.
|
||||
func indexOf(s, substr string) int {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@ -1,148 +0,0 @@
|
||||
//go:build notray
|
||||
|
||||
package tray
|
||||
|
||||
const webGUIHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Edge AI Platform Controller</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: #1a1a2e; color: #e0e0e0;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
min-height: 100vh; padding: 20px;
|
||||
}
|
||||
.card {
|
||||
background: #16213e; border-radius: 12px; padding: 32px;
|
||||
width: 100%; max-width: 420px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||
}
|
||||
h1 {
|
||||
font-size: 18px; font-weight: 600; margin-bottom: 24px;
|
||||
color: #fff; text-align: center;
|
||||
}
|
||||
.status-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 14px 16px; background: #0f3460; border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.dot {
|
||||
width: 12px; height: 12px; border-radius: 50%;
|
||||
flex-shrink: 0; transition: background 0.3s;
|
||||
}
|
||||
.dot.running { background: #00d26a; box-shadow: 0 0 8px rgba(0,210,106,0.5); }
|
||||
.dot.stopped { background: #ff4757; box-shadow: 0 0 8px rgba(255,71,87,0.4); }
|
||||
.status-text { font-size: 14px; }
|
||||
.status-text strong { color: #fff; }
|
||||
.info-row {
|
||||
font-size: 13px; color: #8a9cc5; padding: 6px 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex; gap: 10px; margin-top: 20px;
|
||||
}
|
||||
button {
|
||||
flex: 1; padding: 12px 16px; border: none; border-radius: 8px;
|
||||
font-size: 14px; font-weight: 500; cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
}
|
||||
button:hover { opacity: 0.85; }
|
||||
button:active { transform: scale(0.97); }
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-primary { background: #533483; color: #fff; }
|
||||
.btn-success { background: #00d26a; color: #1a1a2e; }
|
||||
.btn-danger { background: #ff4757; color: #fff; }
|
||||
.btn-secondary { background: #2d3561; color: #e0e0e0; }
|
||||
.btn-quit {
|
||||
margin-top: 12px; width: 100%; background: transparent;
|
||||
border: 1px solid #444; color: #888; font-size: 13px;
|
||||
padding: 8px;
|
||||
}
|
||||
.btn-quit:hover { border-color: #ff4757; color: #ff4757; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Edge AI Platform</h1>
|
||||
|
||||
<div class="status-row">
|
||||
<div id="dot" class="dot stopped"></div>
|
||||
<div class="status-text">
|
||||
Status: <strong id="status-text">Checking...</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="info-relay" class="info-row" style="display:none"></div>
|
||||
|
||||
<div class="buttons">
|
||||
<button id="btn-toggle" class="btn-primary" onclick="toggleServer()" disabled>
|
||||
Start Server
|
||||
</button>
|
||||
<button id="btn-dashboard" class="btn-success" onclick="openDashboard()" disabled>
|
||||
Open Dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="btn-quit" onclick="quitApp()">Quit</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let serverRunning = false;
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const r = await fetch('/api/status');
|
||||
const d = await r.json();
|
||||
serverRunning = d.running;
|
||||
|
||||
document.getElementById('dot').className = 'dot ' + (d.running ? 'running' : 'stopped');
|
||||
document.getElementById('status-text').textContent =
|
||||
d.running ? 'Running (:' + d.port + ')' : 'Stopped';
|
||||
|
||||
const btn = document.getElementById('btn-toggle');
|
||||
btn.disabled = false;
|
||||
btn.textContent = d.running ? 'Stop Server' : 'Start Server';
|
||||
btn.className = d.running ? 'btn-danger' : 'btn-primary';
|
||||
|
||||
document.getElementById('btn-dashboard').disabled = !d.running;
|
||||
|
||||
const relayEl = document.getElementById('info-relay');
|
||||
if (d.relay) {
|
||||
relayEl.style.display = 'block';
|
||||
relayEl.textContent = 'Relay: ' + d.relay;
|
||||
} else {
|
||||
relayEl.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('status-text').textContent = 'Connection lost';
|
||||
document.getElementById('dot').className = 'dot stopped';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleServer() {
|
||||
const btn = document.getElementById('btn-toggle');
|
||||
btn.disabled = true;
|
||||
await fetch(serverRunning ? '/api/stop' : '/api/start', { method: 'POST' });
|
||||
setTimeout(fetchStatus, 1000);
|
||||
}
|
||||
|
||||
async function openDashboard() {
|
||||
await fetch('/api/open-dashboard', { method: 'POST' });
|
||||
}
|
||||
|
||||
async function quitApp() {
|
||||
if (confirm('Stop server and quit?')) {
|
||||
await fetch('/api/quit', { method: 'POST' });
|
||||
document.getElementById('status-text').textContent = 'Shutting down...';
|
||||
}
|
||||
}
|
||||
|
||||
fetchStatus();
|
||||
setInterval(fetchStatus, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
15
edge-ai-platform/server/tray/webgui_other.go
Normal file
15
edge-ai-platform/server/tray/webgui_other.go
Normal file
@ -0,0 +1,15 @@
|
||||
//go:build notray && !windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// RunWebGUI is not supported on non-Windows platforms without CGO.
|
||||
// Use --tray mode instead (requires CGO_ENABLED=1).
|
||||
func RunWebGUI(_ *Config) {
|
||||
fmt.Fprintln(os.Stderr, "GUI mode is only available on Windows. Use --tray mode instead (requires CGO_ENABLED=1).")
|
||||
os.Exit(1)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user