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 { Progress } from '@/components/ui/progress'
import { cn } from '@/lib/utils' 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 // Types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -188,8 +195,9 @@ function ProcessDetailPanel({
const load = useCallback(async () => { const load = useCallback(async () => {
try { try {
// Use low-level Wails IPC — binding will be regenerated automatically const d: ProcessDetail = isWeb
const d = await (window as any)['go']['main']['App']['GetProcessDetail'](pid) as ProcessDetail ? 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) setDetail(d)
} catch { } catch {
setDetail(null) setDetail(null)
@@ -208,7 +216,12 @@ function ProcessDetailPanel({
setKilling(true) setKilling(true)
setKillError(null) setKillError(null)
try { 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 // stop refreshing — process is gone
if (intervalRef.current) clearInterval(intervalRef.current) if (intervalRef.current) clearInterval(intervalRef.current)
onClose() onClose()
@@ -231,10 +244,18 @@ function ProcessDetailPanel({
lastManNameRef.current = detail.name lastManNameRef.current = detail.name
setManPageLoading(true) setManPageLoading(true)
setManPage(null) setManPage(null)
;(window as any)['go']['main']['App']['GetManPage'](detail.name) if (isWeb) {
.then((text: string) => { setManPage(text || null) }) fetch(`${apiBase}/api/man/${encodeURIComponent(detail.name)}`)
.catch(() => setManPage(null)) .then(r => r.ok ? r.text() : Promise.reject())
.finally(() => setManPageLoading(false)) .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]) }, [detail?.name])
return ( return (
@@ -541,6 +562,15 @@ export default function App() {
const [pageSize, setPageSize] = useState(20) const [pageSize, setPageSize] = useState(20)
useEffect(() => { 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) const handler = (m: Metrics) => setMetrics(m)
EventsOn('metrics', handler) EventsOn('metrics', handler)
return () => EventsOff('metrics') return () => EventsOff('metrics')

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@@ -19,6 +20,14 @@ func runServer(port int) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() 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) streamSys := backend.NewSysInfoHeadless(ctx)
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -148,6 +157,19 @@ func runServer(port int) {
fmt.Printf(" GET /api/man/<name> — manual page for a command\n") fmt.Printf(" GET /api/man/<name> — manual page for a command\n")
fmt.Println("\nPress Ctrl+C to stop.") 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{ srv := &http.Server{
Addr: addr, Addr: addr,
Handler: mux, Handler: mux,