Tighten web surfaces and clean handoff

This commit is contained in:
jan
2026-04-20 21:12:52 +02:00
parent ed1e4b49ab
commit d2ca1a2aef
13 changed files with 751 additions and 390 deletions

View File

@@ -28,6 +28,7 @@ const appState = {
snapshot: null,
outputDraft: null,
outputDirty: false,
outputSaving: false,
nodeDrafts: new Map(),
panelDrafts: new Map(),
discovery: {
@@ -181,17 +182,22 @@ async function loadState() {
}
function syncDraftsFromState(snapshot) {
elements.projectName.textContent = `${snapshot.system.project_name} | ${snapshot.system.topology_label}`;
const system = snapshotSystem(snapshot);
const technical = snapshotTechnical(snapshot);
const nodes = snapshotNodes(snapshot);
const panels = snapshotPanels(snapshot);
if (!appState.outputDirty) {
elements.projectName.textContent = `${system.project_name} | ${system.topology_label}`;
if (!appState.outputDirty && !appState.outputSaving) {
appState.outputDraft = {
backend_mode: snapshot.technical.backend_mode,
output_enabled: snapshot.technical.output_enabled,
output_fps: snapshot.technical.output_fps,
backend_mode: technical.backend_mode,
output_enabled: technical.output_enabled,
output_fps: technical.output_fps,
};
}
for (const node of snapshot.nodes) {
for (const node of nodes) {
const existing = appState.nodeDrafts.get(node.node_id);
if (!existing || !existing.dirty) {
appState.nodeDrafts.set(node.node_id, {
@@ -201,7 +207,7 @@ function syncDraftsFromState(snapshot) {
}
}
for (const panel of snapshot.panels) {
for (const panel of panels) {
const key = panelKey(panel.node_id, panel.panel_position);
const existing = appState.panelDrafts.get(key);
if (!existing || !existing.dirty) {
@@ -255,20 +261,22 @@ function renderTopbar() {
return;
}
const onlineCount = snapshot.nodes.filter((node) => node.connection === "online").length;
const technical = snapshotTechnical(snapshot);
const nodes = snapshotNodes(snapshot);
const onlineCount = nodes.filter((node) => node.connection === "online").length;
setChip(
elements.backendPill,
backendModeLabel(snapshot.technical.backend_mode),
snapshot.technical.backend_mode === "preview_only" ? "idle" : "live"
backendModeLabel(technical.backend_mode),
technical.backend_mode === "preview_only" ? "idle" : "live"
);
setChip(
elements.outputPill,
snapshot.technical.output_enabled ? "enabled" : "disabled",
snapshot.technical.output_enabled ? "success" : "warning"
technical.output_enabled ? "enabled" : "disabled",
technical.output_enabled ? "success" : "warning"
);
setChip(
elements.nodesPill,
`${onlineCount}/${snapshot.nodes.length} online`,
`${onlineCount}/${nodes.length} online`,
onlineCount > 0 ? "success" : "warning"
);
}
@@ -278,7 +286,9 @@ function renderSummaryCards() {
elements.summaryGrid.innerHTML = "";
return;
}
const { technical, nodes, panels } = appState.snapshot;
const technical = snapshotTechnical(appState.snapshot);
const nodes = snapshotNodes(appState.snapshot);
const panels = snapshotPanels(appState.snapshot);
const onlineCount = nodes.filter((node) => node.connection === "online").length;
const enabledOutputs = panels.filter((panel) => panel.enabled).length;
const liveOutputs = panels.filter(
@@ -313,10 +323,15 @@ function renderOutputControls() {
elements.backendModeSelect.value = appState.outputDraft.backend_mode;
elements.outputEnabledInput.checked = Boolean(appState.outputDraft.output_enabled);
elements.outputFpsInput.value = String(appState.outputDraft.output_fps);
elements.liveStatus.textContent = appState.snapshot.technical.live_status;
elements.liveStatus.className = `status-banner ${bannerLevelForTechnical(appState.snapshot.technical)}`;
const technical = snapshotTechnical(appState.snapshot);
elements.liveStatus.textContent = technical.live_status;
elements.liveStatus.className = `status-banner ${bannerLevelForTechnical(technical)}`;
elements.backendSemantics.textContent = backendSemanticsText(appState.outputDraft);
elements.saveOutputSettingsButton.disabled = !appState.outputDirty;
const controlsDisabled = appState.outputSaving;
elements.backendModeSelect.disabled = controlsDisabled;
elements.outputEnabledInput.disabled = controlsDisabled;
elements.outputFpsInput.disabled = controlsDisabled;
elements.saveOutputSettingsButton.disabled = controlsDisabled || !appState.outputDirty;
}
function renderNodeTable() {
@@ -325,7 +340,7 @@ function renderNodeTable() {
return;
}
elements.nodeTableBody.innerHTML = appState.snapshot.nodes
elements.nodeTableBody.innerHTML = snapshotNodes(appState.snapshot)
.map((node) => {
const draft = ensureNodeDraft(node.node_id);
return `
@@ -368,7 +383,7 @@ function renderPanelTable() {
return;
}
elements.panelTableBody.innerHTML = appState.snapshot.panels
elements.panelTableBody.innerHTML = snapshotPanels(appState.snapshot)
.map((panel) => {
const key = panelKey(panel.node_id, panel.panel_position);
const draft = ensurePanelDraft(key, panel);
@@ -505,7 +520,8 @@ function renderEvents() {
return;
}
elements.eventList.innerHTML = appState.snapshot.recent_events
const recentEvents = snapshotRecentEvents(appState.snapshot);
elements.eventList.innerHTML = recentEvents
.map(
(event) => `
<article class="event-entry">
@@ -605,11 +621,11 @@ async function applyDiscoveryAssignment(ip) {
}
async function saveOutputSettings() {
if (!appState.snapshot) {
if (!appState.snapshot || appState.outputSaving) {
return;
}
ensureOutputDraft();
const current = appState.snapshot.technical;
const current = snapshotTechnical(appState.snapshot);
const draft = appState.outputDraft;
const commands = [];
@@ -638,16 +654,24 @@ async function saveOutputSettings() {
return;
}
for (const command of commands) {
const response = await sendCommand(command.type, command.payload);
if (!response.ok) {
return;
}
}
appState.outputSaving = true;
renderOutputControls();
appState.outputDirty = false;
setFeedback("success", "Backend/output settings applied.");
await loadState();
try {
for (const command of commands) {
const response = await sendCommand(command.type, command.payload);
if (!response.ok) {
return;
}
}
appState.outputDirty = false;
setFeedback("success", "Backend/output settings applied.");
await loadState();
} finally {
appState.outputSaving = false;
renderOutputControls();
}
}
async function saveNode(nodeId) {
@@ -773,6 +797,34 @@ function ensureOutputDraft() {
}
}
function snapshotSystem(snapshot) {
return snapshot?.system ?? {
project_name: "Infinity Vis",
topology_label: "Technical surface waiting for snapshot",
};
}
function snapshotTechnical(snapshot) {
return snapshot?.technical ?? {
backend_mode: "preview_only",
output_enabled: false,
output_fps: 40,
live_status: "Waiting for technical state...",
};
}
function snapshotNodes(snapshot) {
return Array.isArray(snapshot?.nodes) ? snapshot.nodes : [];
}
function snapshotPanels(snapshot) {
return Array.isArray(snapshot?.panels) ? snapshot.panels : [];
}
function snapshotRecentEvents(snapshot) {
return Array.isArray(snapshot?.recent_events) ? snapshot.recent_events : [];
}
function ensureNodeDraft(nodeId) {
const existing = appState.nodeDrafts.get(nodeId);
if (existing) {
@@ -806,12 +858,12 @@ function assignedNodeForIp(ip) {
if (!appState.snapshot || !ip) {
return null;
}
const match = appState.snapshot.nodes.find((node) => node.reserved_ip === ip);
const match = snapshotNodes(appState.snapshot).find((node) => node.reserved_ip === ip);
return match ? match.node_id : null;
}
function renderNodeOptions(selectedNodeId) {
const nodes = appState.snapshot ? appState.snapshot.nodes : [];
const nodes = snapshotNodes(appState.snapshot);
return nodes
.map((node) => {
const selected = node.node_id === selectedNodeId ? "selected" : "";