MHRN-Space / src /dashboard /static /frontend /modules /functional-completion.js
ThomasHeisig's picture
Sync GitHub main 4645f4137666463023fa501984a14336dd2fc666 (part 11)
31226fd verified
Raw History Blame Contribute Delete
10.7 kB
"use strict";
const VIEWER_PROFILE_KEY = "mhrn.neuron-model-viewer.profile.v1";
const RATE_REFRESH_MS = 5000;
const EMBODIMENT_REFRESH_MS = 5000;
let initialized = false;
let viewerTimer = null;
let embodimentTimer = null;
let rootObserver = null;
let profileRestored = false;
const byId = (id) => document.getElementById(id);
async function readJson(url) {
const response = await fetch(url, { cache: "no-store", headers: { Accept: "application/json" } });
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
return payload;
}
function finite(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function formatRate(value) {
if (!Number.isFinite(value)) return "—";
if (value >= 100) return `${value.toFixed(1)} Hz`;
if (value >= 10) return `${value.toFixed(2)} Hz`;
return `${value.toFixed(3)} Hz`;
}
function findViewerStat(label) {
return [...document.querySelectorAll("#neuron-model-viewer .nmv-stat")]
.find((node) => node.querySelector("span")?.textContent?.trim() === label) || null;
}
function setProfileStatus(text, title = "") {
const node = byId("nmv-profile-status");
if (!node) return;
if (node.textContent !== text) node.textContent = text;
node.title = title || "Lokales presentation-only Ansichtsprofil; keine Runtime- oder Evidenzdaten.";
}
function currentViewerProfile() {
return {
version: 1,
method: byId("nmv-method")?.value || "pca",
dimensions: byId("nmv-dim")?.value || "2",
sample: byId("nmv-sample")?.value || "500",
interval: byId("nmv-interval")?.value || "0",
view: document.querySelector("#neuron-model-viewer [data-nmv-view].active")?.dataset.nmvView || "projection",
saved_at: new Date().toISOString(),
scope: "presentation_only",
};
}
function saveViewerProfile() {
if (!byId("neuron-model-viewer")) return;
try {
const profile = currentViewerProfile();
localStorage.setItem(VIEWER_PROFILE_KEY, JSON.stringify(profile));
setProfileStatus("gespeichert · lokal", `Gespeichert ${new Date(profile.saved_at).toLocaleString("de-DE")} · presentation-only`);
} catch (error) {
setProfileStatus("Speicher nicht verfügbar", String(error.message || error));
}
}
function restoreViewerProfile() {
if (profileRestored || !byId("neuron-model-viewer")) return;
profileRestored = true;
let profile = null;
try { profile = JSON.parse(localStorage.getItem(VIEWER_PROFILE_KEY) || "null"); } catch (_) {}
if (!profile || typeof profile !== "object") {
setProfileStatus("bereit · lokal", "Noch kein Ansichtsprofil gespeichert. Änderungen werden automatisch lokal gespeichert.");
return;
}
const assign = (id, value, emit = true) => {
const control = byId(id);
if (!control || ![...control.options].some((option) => option.value === String(value))) return;
control.value = String(value);
if (emit) control.dispatchEvent(new Event("change", { bubbles: true }));
};
// Method is restored without firing change: nonlinear backend jobs must never
// start merely because the page was opened.
assign("nmv-method", profile.method || "pca", false);
assign("nmv-dim", profile.dimensions || "2");
assign("nmv-sample", profile.sample || "500");
assign("nmv-interval", profile.interval || "0");
const view = document.querySelector(`#neuron-model-viewer [data-nmv-view="${CSS.escape(String(profile.view || "projection"))}"]`);
view?.click();
setProfileStatus("wiederhergestellt · lokal", "Presentation-only Ansichtsprofil aus localStorage wiederhergestellt.");
}
function bindViewerProfile(viewer) {
if (viewer.dataset.functionalCompletionBound === "true") return;
viewer.dataset.functionalCompletionBound = "true";
["nmv-method", "nmv-dim", "nmv-sample", "nmv-interval"].forEach((id) => {
byId(id)?.addEventListener("change", () => {
saveViewerProfile();
if (id === "nmv-sample") void refreshNeuronRates();
});
});
viewer.querySelector(".nmv-view-tabs")?.addEventListener("click", (event) => {
if (event.target.closest("[data-nmv-view]")) queueMicrotask(saveViewerProfile);
});
}
function ensureViewerCompletion() {
const viewer = byId("neuron-model-viewer");
if (!viewer) return false;
if (viewer.dataset.functionalCompletionReady === "true") return true;
const rateStat = findViewerStat("Per-neuron Hz");
if (rateStat) {
const label = rateStat.querySelector("span");
const value = rateStat.querySelector("strong");
if (label) {
label.textContent = "Per-neuron Hz · kumulativ";
label.title = "Kumulative Feuerrate seit Runtime-Start; keine Instantanrate.";
}
if (value) {
value.id = "nmv-rate-summary";
value.classList.remove("nmv-pending");
value.textContent = "Messfenster wird geladen …";
}
}
const profileStat = findViewerStat("Profil-Cache");
if (profileStat) {
const label = profileStat.querySelector("span");
const value = profileStat.querySelector("strong");
if (label) {
label.textContent = "Ansichtsprofil";
label.title = "Presentation-only Zustand des Viewers; keine wissenschaftlichen Daten.";
}
if (value) {
value.id = "nmv-profile-status";
value.classList.remove("nmv-pending");
value.textContent = "lokal · presentation-only";
}
}
const legend = viewer.querySelector(".nmv-legend");
if (legend) {
const marker = legend.querySelector("i");
legend.replaceChildren(document.createTextNode("Feuerrate · kumulativ "));
if (marker) legend.append(marker);
legend.title = "Bei festem Beobachtungsfenster ist die Spike-Counter-Farbskala monoton äquivalent zur kumulativen Feuerrate.";
}
bindViewerProfile(viewer);
restoreViewerProfile();
viewer.dataset.functionalCompletionReady = "true";
return true;
}
async function refreshNeuronRates() {
if (!ensureViewerCompletion()) return;
const output = byId("nmv-rate-summary");
if (!output) return;
const requested = Math.max(1, Math.min(2000, Number(byId("nmv-sample")?.value || 500)));
try {
const [summary, projection] = await Promise.all([
readJson("/api/network/summary"),
readJson(`/api/network/projection?limit=${requested}&mode=activity`),
]);
const tick = finite(summary.current_tick);
const dtMs = 1.0; // Reference core invariant: SimulationConfig enforces dt_ms == 1.0.
if (tick === null || tick <= 0) {
output.textContent = "noch kein Messfenster · Tick 0";
output.title = "Eine Feuerrate benötigt ein positives Beobachtungsfenster.";
return;
}
const durationSeconds = tick * dtMs / 1000;
const rates = (projection.points || [])
.map((point) => finite(point.value))
.filter((value) => value !== null)
.map((spikeCount) => spikeCount / durationSeconds);
if (!rates.length) {
output.textContent = "keine Neuronendaten";
return;
}
const mean = rates.reduce((sum, value) => sum + value, 0) / rates.length;
output.textContent = `μ ${formatRate(mean)} · ${formatRate(Math.min(...rates))}–${formatRate(Math.max(...rates))} · n=${rates.length}`;
output.title = `Kumulative Feuerrate über ${tick.toLocaleString("de-DE")} Ticks (${durationSeconds.toFixed(3)} s). Quelle: Live-Runtime. Kein EVIDENCE-Claim.`;
} catch (error) {
output.textContent = `nicht verfügbar · ${error.message}`;
output.title = "Live-Raten konnten nicht aus den Runtime-Endpunkten berechnet werden.";
}
}
function feedbackStatus(pipeline, state) {
if (state?.last_observation_state) return ["observation received", `Rückkopplung gemessen: ${state.last_observation_state}`];
const stage = pipeline?.stages?.feedback;
if (!stage) return ["state unknown", "Der Pipeline-Vertrag hat keinen Feedback-Status geliefert."];
if (stage.implemented === false) return ["adapter feedback unavailable", "Die aktuelle Adapter-/Runtime-Konfiguration veröffentlicht keinen implementierten Feedback-Pfad."];
if (stage.enabled) return ["enabled · waiting for observation", "Feedback-Pipeline ist freigegeben; noch keine EnvironmentObservation empfangen."];
return ["ready · disabled", "Feedback-Pipeline ist implementiert, aktuell aber deaktiviert."];
}
async function refreshEmbodimentFeedback() {
const node = byId("embodiment-feedback-state");
if (!node) return;
try {
const [pipeline, state] = await Promise.all([
readJson("/api/embodiment/pipeline"),
readJson("/api/embodiment/state"),
]);
const [text, title] = feedbackStatus(pipeline, state);
if (node.textContent !== text) node.textContent = text;
node.title = title;
node.dataset.functionalState = text.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
} catch (error) {
if (node.textContent !== "state unavailable") node.textContent = "state unavailable";
node.title = `Embodiment-Status nicht erreichbar: ${error.message}`;
}
}
function annotateGuardedDisabledControls() {
const experimentStop = byId("experiment-stop");
if (experimentStop?.disabled && !experimentStop.title) {
experimentStop.title = "Wird automatisch aktiviert, sobald eine Experiment-Session aktiv ist.";
}
document.querySelectorAll("button:disabled, input:disabled, select:disabled, option:disabled").forEach((control) => {
if (!control.title && control.getAttribute("aria-label")) control.title = control.getAttribute("aria-label");
});
}
function scanDynamicSurfaces() {
ensureViewerCompletion();
annotateGuardedDisabledControls();
}
export function initFunctionalCompletion() {
if (initialized) {
scanDynamicSurfaces();
void refreshNeuronRates();
void refreshEmbodimentFeedback();
return;
}
initialized = true;
scanDynamicSurfaces();
void refreshNeuronRates();
void refreshEmbodimentFeedback();
rootObserver = new MutationObserver(() => {
if (!byId("neuron-model-viewer")?.dataset.functionalCompletionReady) scanDynamicSurfaces();
});
rootObserver.observe(document.querySelector("main") || document.body, { childList: true, subtree: true });
viewerTimer = window.setInterval(refreshNeuronRates, RATE_REFRESH_MS);
embodimentTimer = window.setInterval(refreshEmbodimentFeedback, EMBODIMENT_REFRESH_MS);
window.addEventListener("beforeunload", () => {
if (viewerTimer) clearInterval(viewerTimer);
if (embodimentTimer) clearInterval(embodimentTimer);
rootObserver?.disconnect();
}, { once: true });
window.MHRNFunctionalCompletion = {
refresh: () => {
scanDynamicSurfaces();
void refreshNeuronRates();
void refreshEmbodimentFeedback();
},
viewerProfileKey: VIEWER_PROFILE_KEY,
};
}