feat: implement web mode for process detail fetching and management, enhance server routing for SPA
This commit is contained in:
@@ -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')
|
||||
|
||||
22
server.go
22
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/<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,
|
||||
|
||||
Reference in New Issue
Block a user