feat(data-only): GUIs synchronisees + audit

Web data-only fully wired contre les 20 feeds :

dashboard.js : 8 nouveaux handlers (gdelt, wikimedia, tides,
moon, atc, pose, mempool, github, rte_eco2mix). Sparklines +
seuils alert/warn/green coherents.

map.js : marker GDELT (purple) en plus des seismes/foudre/
avions/ISS/volcans, TTL 60s comme les autres ephemeres.

control.html : nouvelle section 'Modes visuels (oF / Metal)'
avec 9 boutons (storm/tunnel/plasma/kaleido/voronoi/metaballs/
stars/bars/hands3d). Le handler scene-btn est splitte par
container (#scenes vs #vizmodes) pour ne pas melanger
l'etat actif.

server.js : whitelist /control/viz* + routing par path. Les
/control/viz* partent vers oF (:57123), tout le reste vers
SC (:57121). Nouveau env OF_PORT_OUT.

Audit fixes :
- LaunchMode displayName Data-only : 'SC + oF + feeds, no web'
  -> 'SC + Metal viz + feeds + web' (l'ancien libelle datait
  d'avant le toggle Web UI dans dataOnly).
- SettingsPanel : toggle Squelette etiquete '(a venir)' tant
  que le rendu n'est pas implemente cote MeshRenderer.
This commit is contained in:
L'électron rare
2026-05-13 19:56:17 +02:00
parent b361c1c09b
commit 4fd37b4faf
8 changed files with 143 additions and 5 deletions
+13
View File
@@ -168,6 +168,19 @@
<button class="scene-btn" data-scene="full">full</button>
<button class="scene-btn" data-scene="stop">stop</button>
</div>
<h2 style="margin-top:18px">Modes visuels (oF / Metal)</h2>
<div class="scene-grid" id="vizmodes">
<button class="scene-btn" data-viz="0">storm</button>
<button class="scene-btn" data-viz="1">tunnel</button>
<button class="scene-btn" data-viz="2">plasma</button>
<button class="scene-btn" data-viz="3">kaleido</button>
<button class="scene-btn" data-viz="4">voronoi</button>
<button class="scene-btn" data-viz="5">metaballs</button>
<button class="scene-btn" data-viz="6">stars</button>
<button class="scene-btn" data-viz="7">bars</button>
<button class="scene-btn" data-viz="8">hands3d</button>
</div>
<h2 style="margin-top:18px">Retour SC</h2>
<div class="sync-row">BPM <b id="sync-bpm"></b></div>
<div class="sync-row">Beat <b id="sync-beat"></b></div>
+12 -2
View File
@@ -48,15 +48,25 @@ document.querySelectorAll(".slider-row").forEach((row) => {
});
// ---------- Scene buttons ----------
document.querySelectorAll(".scene-btn").forEach((btn) => {
document.querySelectorAll("#scenes .scene-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".scene-btn")
document.querySelectorAll("#scenes .scene-btn")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
send("/scene/play", [btn.dataset.scene]);
});
});
// ---------- Visual mode buttons ----------
document.querySelectorAll("#vizmodes .scene-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll("#vizmodes .scene-btn")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
send("/control/vizMode", [parseInt(btn.dataset.viz, 10)]);
});
});
// ---------- XY pad ----------
const pad = document.getElementById("xy");
const cursor = document.getElementById("xy-cursor");
+101
View File
@@ -235,6 +235,107 @@ const handlers = {
drawSpark(c);
}
},
// ---- Nouveaux feeds (gdelt, wikimedia, tides, atc, pose, mempool,
// github, rte_eco2mix) -------------------------------------
gdelt: (m) => {
if (m.sub === "batch") {
const [n, countries, tone] = m.args;
const c = ensureCard("gdelt", { title: "GDELT · evenements 15min",
order: 19, wide: true,
alert: tone < -5, warn: tone < -2, green: tone > 2 });
c.valueEl.textContent = String(n | 0);
c.subEl.textContent = `${countries | 0} pays · tone ${fmt(tone, 2)}`;
pushHistory(c, n);
drawSpark(c);
}
},
wikimedia: (m) => {
if (m.sub === "rate") {
const c = ensureCard("wiki", { title: "Wikipedia firehose", order: 20 });
c.valueEl.textContent = `${fmt(m.args[0], 1)} /s`;
pushHistory(c, m.args[0]);
drawSpark(c, "#6fa6ff");
} else if (m.sub === "edit") {
const c = ensureCard("wiki", { title: "Wikipedia firehose", order: 20 });
c.subEl.textContent = `${m.args[0]}: ${m.args[1]}`;
}
},
tides: (m) => {
if (m.sub === "level") {
const [obs, pred, residual] = m.args;
const c = ensureCard("tides", { title: "Marees NOAA", order: 21, wide: true });
c.valueEl.textContent = `${fmt(obs, 2)} m`;
c.subEl.textContent = `predit ${fmt(pred, 2)} m · residual ${fmt(residual, 2)} m`;
pushHistory(c, obs);
drawSpark(c, "#6fa6ff");
} else if (m.sub === "moon") {
const [phase, illum] = m.args;
const c = ensureCard("moon", { title: "Phase lunaire", order: 22 });
const phaseName = phase < 0.05 || phase > 0.95 ? "nouvelle"
: phase < 0.25 ? "premier croissant"
: phase < 0.30 ? "premier quartier"
: phase < 0.45 ? "gibbeuse croissante"
: phase < 0.55 ? "pleine"
: phase < 0.70 ? "gibbeuse decroissante"
: phase < 0.80 ? "dernier quartier"
: "dernier croissant";
c.valueEl.textContent = `${fmt(illum * 100, 0)}%`;
c.subEl.textContent = phaseName;
}
},
atc: (m) => {
if (m.sub === "total") {
const [total, hubs] = m.args;
const c = ensureCard("atc", { title: "ATC · auditeurs", order: 23 });
c.valueEl.textContent = String(total | 0);
c.subEl.textContent = `${hubs | 0} hubs actifs`;
pushHistory(c, total);
drawSpark(c, "#6fe9b3");
}
},
pose: (m) => {
if (m.sub === "count") {
const c = ensureCard("pose", { title: "Pose YOLO · personnes", order: 3 });
c.valueEl.textContent = String(m.args[0] | 0);
pushHistory(c, m.args[0]);
drawSpark(c, "#ff8838");
}
},
mempool: (m) => {
if (m.sub === "fee") {
const c = ensureCard("btc", { title: "Bitcoin · fee sat/vB", order: 24 });
c.valueEl.textContent = String(m.args[0] | 0);
pushHistory(c, m.args[0]);
drawSpark(c, "#ff8838");
} else if (m.sub === "tx") {
const c = ensureCard("btc", { title: "Bitcoin · fee sat/vB", order: 24 });
c.subEl.textContent = `${m.args[0] | 0} tx en attente`;
}
},
github: (m) => {
if (m.sub === "rate") {
const c = ensureCard("gh", { title: "GitHub events", order: 25 });
c.valueEl.textContent = `${fmt(m.args[0], 1)} /s`;
pushHistory(c, m.args[0]);
drawSpark(c, "#6fa6ff");
}
},
rte_eco2mix: (m) => {
if (m.sub === "mix") {
const [nuclear, solar, wind, gas] = m.args;
const c = ensureCard("rte", { title: "RTE eCO2mix · GW", order: 26, wide: true });
c.valueEl.textContent = `${fmt((nuclear + solar + wind + gas) / 1000, 1)} GW`;
c.subEl.textContent = `nuc ${fmt(nuclear / 1000, 1)} · sol ${fmt(solar / 1000, 1)} · eol ${fmt(wind / 1000, 1)} · gaz ${fmt(gas / 1000, 1)}`;
pushHistory(c, (nuclear + solar + wind + gas) / 1000);
drawSpark(c, "#6fe9b3");
} else if (m.sub === "co2") {
const c = ensureCard("co2", { title: "Intensite CO2 · g/kWh", order: 27,
alert: m.args[0] > 200, warn: m.args[0] > 100, green: m.args[0] < 50 });
c.valueEl.textContent = String(m.args[0] | 0);
pushHistory(c, m.args[0]);
drawSpark(c);
}
},
};
function handleMessage(msg) {
+7
View File
@@ -87,6 +87,13 @@ const handlers = {
ephemeralMarker(lat, lon, "volcano",
`volcan ${region} (VEI ${vei})`);
},
gdelt: (m) => {
if (m.sub !== "event") return;
const [lat, lon, tone, country] = m.args;
if (!lat && !lon) return;
const tip = `GDELT ${country || "?"} (tone ${(+tone).toFixed(1)})`;
ephemeralMarker(lat, lon, "gdelt", tip);
},
};
function handleMessage(msg) {
+1
View File
@@ -27,6 +27,7 @@
.plane { background: #6fe9b3; border-radius: 50%; }
.iss { background: #6fa6ff; border-radius: 50%; border: 2px solid #fff; }
.volcano { background: #ff8838; border-radius: 50%; }
.gdelt { background: #c878ff; border-radius: 50%; opacity: 0.8; }
</style>
</head>
<body>
+7 -1
View File
@@ -25,6 +25,7 @@ const OSC_DATA_IN = parseInt(process.env.OSC_DATA_IN ?? "57124", 10);
const OSC_SYNC_IN = parseInt(process.env.OSC_SYNC_IN ?? "57125", 10);
const SC_HOST = process.env.SC_HOST ?? "127.0.0.1";
const SC_PORT_OUT = parseInt(process.env.SC_PORT_OUT ?? "57121", 10);
const OF_PORT_OUT = parseInt(process.env.OF_PORT_OUT ?? "57123", 10);
const app = express();
app.use(express.static(join(__dirname, "public")));
@@ -69,7 +70,12 @@ wss.on("connection", (ws) => {
if (typeof v === "number") return { type: "f", value: v };
return { type: "s", value: String(v) };
});
udpOut.send({ address: msg.path, args }, SC_HOST, SC_PORT_OUT);
// Routing par path :
// /control/viz* -> oscope-of (:57123) — Metal viz + skeleton renderer
// tout le reste -> SC (:57121)
const port = msg.path.startsWith("/control/viz")
? OF_PORT_OUT : SC_PORT_OUT;
udpOut.send({ address: msg.path, args }, SC_HOST, port);
});
});
@@ -64,7 +64,7 @@ struct SettingsPanel: View {
label: "Fil de fer",
isOn: $settings.showWireframe)
layerRow(icon: "figure.stand",
label: "Squelette (articulations)",
label: "Squelette (à venir)",
isOn: $settings.showSkeleton)
}
}
@@ -17,7 +17,7 @@ enum LaunchMode: String, CaseIterable, Identifiable {
var displayName: String {
switch self {
case .full: return "Full AV-Live"
case .dataOnly: return "Data-only (SC + oF + feeds, no web)"
case .dataOnly: return "Data-only (SC + Metal viz + feeds + web)"
case .bodyMesh: return "Body Mesh (Multi-HMR + RealityKit)"
}
}