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 { 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 {
|
||||||
|
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)
|
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)
|
||||||
|
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)
|
;(window as any)['go']['main']['App']['GetManPage'](detail.name)
|
||||||
.then((text: string) => { setManPage(text || null) })
|
.then((text: string) => { setManPage(text || null) })
|
||||||
.catch(() => setManPage(null))
|
.catch(() => setManPage(null))
|
||||||
.finally(() => setManPageLoading(false))
|
.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')
|
||||||
|
|||||||
22
server.go
22
server.go
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user