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:
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user