Tighten web surfaces and clean handoff
This commit is contained in:
@@ -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" : "";
|
||||
|
||||
Reference in New Issue
Block a user