diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 73ee0b9..4c7a1eb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,13 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { cn } from '@/lib/utils' +// Detect if running inside Wails desktop or as a standalone web app +const isWeb = !(window as any).__wails_ipc_active && + typeof (window as any)['go'] === 'undefined' + +// Base URL for web mode: same origin as the page +const apiBase = `${window.location.protocol}//${window.location.host}` + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -188,8 +195,9 @@ function ProcessDetailPanel({ const load = useCallback(async () => { try { - // Use low-level Wails IPC — binding will be regenerated automatically - const d = await (window as any)['go']['main']['App']['GetProcessDetail'](pid) as ProcessDetail + const d: ProcessDetail = isWeb + ? await fetch(`${apiBase}/api/process/${pid}`).then(r => { if (!r.ok) throw new Error(); return r.json() }) + : await (window as any)['go']['main']['App']['GetProcessDetail'](pid) as ProcessDetail setDetail(d) } catch { setDetail(null) @@ -208,7 +216,12 @@ function ProcessDetailPanel({ setKilling(true) setKillError(null) try { - await (window as any)['go']['main']['App']['KillProcess'](pid, force) + if (isWeb) { + const res = await fetch(`${apiBase}/api/process/${pid}/kill${force ? '?force=true' : ''}`, { method: 'POST' }) + if (!res.ok) throw new Error(await res.text()) + } else { + await (window as any)['go']['main']['App']['KillProcess'](pid, force) + } // stop refreshing — process is gone if (intervalRef.current) clearInterval(intervalRef.current) onClose() @@ -231,10 +244,18 @@ function ProcessDetailPanel({ lastManNameRef.current = detail.name setManPageLoading(true) setManPage(null) - ;(window as any)['go']['main']['App']['GetManPage'](detail.name) - .then((text: string) => { setManPage(text || null) }) - .catch(() => setManPage(null)) - .finally(() => setManPageLoading(false)) + if (isWeb) { + fetch(`${apiBase}/api/man/${encodeURIComponent(detail.name)}`) + .then(r => r.ok ? r.text() : Promise.reject()) + .then(text => setManPage(text || null)) + .catch(() => setManPage(null)) + .finally(() => setManPageLoading(false)) + } else { + ;(window as any)['go']['main']['App']['GetManPage'](detail.name) + .then((text: string) => { setManPage(text || null) }) + .catch(() => setManPage(null)) + .finally(() => setManPageLoading(false)) + } }, [detail?.name]) return ( @@ -541,6 +562,15 @@ export default function App() { const [pageSize, setPageSize] = useState(20) useEffect(() => { + if (isWeb) { + // Web mode: connect to SSE stream + const es = new EventSource(`${apiBase}/api/metrics/stream`) + es.onmessage = (e) => { + try { setMetrics(JSON.parse(e.data) as Metrics) } catch {} + } + return () => es.close() + } + // Wails desktop mode: listen to pushed events const handler = (m: Metrics) => setMetrics(m) EventsOn('metrics', handler) return () => EventsOff('metrics') diff --git a/server.go b/server.go index a4abc2b..9c70e45 100644 --- a/server.go +++ b/server.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io/fs" "net/http" "os" "os/signal" @@ -19,6 +20,14 @@ func runServer(port int) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // Serve embedded frontend/dist at / + distFS, err := fs.Sub(assets, "frontend/dist") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load embedded assets: %v\n", err) + os.Exit(1) + } + fileServer := http.FileServer(http.FS(distFS)) + streamSys := backend.NewSysInfoHeadless(ctx) mux := http.NewServeMux() @@ -148,6 +157,19 @@ func runServer(port int) { fmt.Printf(" GET /api/man/ — manual page for a command\n") fmt.Println("\nPress Ctrl+C to stop.") + // Fallback: serve index.html for any non-API, non-file route (SPA) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Try to serve the exact file from dist; if it doesn't exist, serve index.html + _, err := distFS.Open(strings.TrimPrefix(r.URL.Path, "/")) + if err != nil || r.URL.Path == "/" { + r2 := r.Clone(r.Context()) + r2.URL.Path = "/" + fileServer.ServeHTTP(w, r2) + return + } + fileServer.ServeHTTP(w, r) + }) + srv := &http.Server{ Addr: addr, Handler: mux,