feat(data-only): dashboard web + carte monde
Pipeline web data-only complet : Cote feeds, OSC target :57124 ajoute au profil data-only.toml pour que le bridge.py diffuse vers : SC (57121) + oF (57123) + Web (57124). Cote web (data_only_viz/web/) : - server.js : Express :3211 + WS broadcast + osc UDP :57124 parse les paths /data/<feed>/<sub> en JSON et diffuse - public/dashboard.html : grille de cards live avec sparklines SVG vanilla (USGS, SWPC Kp/wind/Bz/X-ray, Blitz, OpenSky, Bluesky, meteo, air, ISS, volcans, social_buzz, grid) - public/map.html : Leaflet dark fullscreen avec markers ephemeres (60s TTL) pour seismes/foudre/avions/volcans + marker ISS persistant qui suit la position Cote launcher : - ProcessManager : startDataWeb / stopDataWeb + openDataDashboard / openDataMap, port :3211 hardcode, script attendu a metalVizDir/web/server.js - MenuBarContent : nouvelle row 'Dashboard data-only' en mode .dataOnly et .bodyMesh + boutons Dashboard/Carte monde quand le serveur tourne - stopAll inclut dataWebProc
This commit is contained in:
@@ -118,6 +118,31 @@ struct MenuBarContent: View {
|
||||
)
|
||||
}
|
||||
|
||||
// Dashboard web data-only (Express :3211, dashboard + carte
|
||||
// monde live OSC). Visible en dataOnly + bodyMesh.
|
||||
if processManager.mode == .dataOnly
|
||||
|| processManager.mode == .bodyMesh {
|
||||
ProcessRow(
|
||||
title: "Dashboard data-only",
|
||||
subtitle: "Express :\(processManager.dataWebPort) — dashboard + carte monde",
|
||||
isRunning: processManager.dataWebRunning,
|
||||
start: processManager.startDataWeb,
|
||||
stop: processManager.stopDataWeb
|
||||
)
|
||||
if processManager.dataWebRunning {
|
||||
HStack {
|
||||
Button(action: processManager.openDataDashboard) {
|
||||
Label("Dashboard", systemImage: "gauge")
|
||||
}
|
||||
Button(action: processManager.openDataMap) {
|
||||
Label("Carte monde", systemImage: "globe")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
if processManager.mode == .full {
|
||||
HStack {
|
||||
Button(action: processManager.openBrowser) {
|
||||
|
||||
@@ -291,6 +291,73 @@ final class ProcessManager: ObservableObject {
|
||||
|
||||
private var webWantsRestart = false
|
||||
|
||||
// ----------------- Web data-only dashboard ------------------------
|
||||
|
||||
private var dataWebProc: Process?
|
||||
@Published var dataWebRunning = false
|
||||
let dataWebPort: Int = 3211
|
||||
|
||||
/// Lance le dashboard web data-only (Express + WS bridge sur :3211,
|
||||
/// ecoute OSC UDP :57124). Necessite node + le dossier
|
||||
/// `data_only_viz/web/`.
|
||||
func startDataWeb() {
|
||||
guard dataWebProc == nil else { return }
|
||||
guard FileManager.default.isExecutableFile(atPath: nodePath) else {
|
||||
append(source: "launcher", text: "node not found at \(nodePath)")
|
||||
return
|
||||
}
|
||||
let script = URL(fileURLWithPath: metalVizDir)
|
||||
.appendingPathComponent("web/server.js").path
|
||||
guard FileManager.default.fileExists(atPath: script) else {
|
||||
append(source: "launcher",
|
||||
text: "data-only web server.js manquant a \(script)")
|
||||
return
|
||||
}
|
||||
let p = Process()
|
||||
p.executableURL = URL(fileURLWithPath: nodePath)
|
||||
p.arguments = [script]
|
||||
p.currentDirectoryURL = URL(fileURLWithPath: script)
|
||||
.deletingLastPathComponent()
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
env["PATH"] = (env["PATH"] ?? "/usr/bin:/bin")
|
||||
+ ":/usr/local/bin:/opt/homebrew/bin"
|
||||
env["HTTP_PORT"] = String(dataWebPort)
|
||||
p.environment = env
|
||||
attach(process: p, label: "dataweb")
|
||||
do {
|
||||
try p.run()
|
||||
dataWebProc = p
|
||||
DispatchQueue.main.async { self.dataWebRunning = true }
|
||||
p.terminationHandler = { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
self?.dataWebProc = nil
|
||||
self?.dataWebRunning = false
|
||||
}
|
||||
}
|
||||
append(source: "launcher",
|
||||
text: "started data-only web on :\(dataWebPort)")
|
||||
} catch {
|
||||
append(source: "launcher",
|
||||
text: "data-only web start failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func stopDataWeb() {
|
||||
dataWebProc?.terminate()
|
||||
}
|
||||
|
||||
func openDataDashboard() {
|
||||
if let url = URL(string: "http://127.0.0.1:\(dataWebPort)/dashboard.html") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
func openDataMap() {
|
||||
if let url = URL(string: "http://127.0.0.1:\(dataWebPort)/map.html") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
func restartWeb() {
|
||||
guard webProc != nil else { startWeb(); return }
|
||||
append(source: "launcher", text: "restarting web server…")
|
||||
@@ -737,6 +804,7 @@ final class ProcessManager: ObservableObject {
|
||||
dataFeedsProc?.terminate()
|
||||
metalVizProc?.terminate()
|
||||
bodyAppProc?.terminate()
|
||||
dataWebProc?.terminate()
|
||||
}
|
||||
|
||||
func clearLogs() {
|
||||
|
||||
Reference in New Issue
Block a user