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:
L'électron rare
2026-05-13 16:50:54 +02:00
parent 39d8739f4c
commit 0eebcb3aec
11 changed files with 1985 additions and 0 deletions
@@ -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() {