feat: implement web mode for process detail fetching and management, enhance server routing for SPA

This commit is contained in:
Jonathan Atta
2026-03-12 09:29:37 +01:00
parent 3ad8bf68a4
commit 748e589159
2 changed files with 59 additions and 7 deletions

View File

@@ -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')

View File

@@ -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/<name> — 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,