Tighten web surfaces and clean handoff
This commit is contained in:
180
web/v1/app.js
180
web/v1/app.js
@@ -1,9 +1,9 @@
|
||||
(function () {
|
||||
const PREVIEW_RENDER_INTERVAL_MS = 90;
|
||||
const PREVIEW_RENDER_INTERVAL_MS = 16;
|
||||
const STATE_RENDER_INTERVAL_MS = 180;
|
||||
const POSITION_ORDER = { top: 0, middle: 1, bottom: 2 };
|
||||
const COLOR_PARAM_KEYS = new Set(["color_mode", "palette", "primary_color", "secondary_color"]);
|
||||
const BRIGHTNESS_PARAM_KEYS = new Set(["brightness"]);
|
||||
const HIDDEN_PARAMETER_KEYS = new Set(["speed", "brightness"]);
|
||||
const TEMPO_BPM_MIN = 10;
|
||||
const TEMPO_BPM_MAX = 300;
|
||||
const TEMPO_BPM_DEFAULT = 120;
|
||||
@@ -46,16 +46,60 @@
|
||||
palette: {
|
||||
control: "enum",
|
||||
label: "Palette",
|
||||
default_value: "Laser Club",
|
||||
default_value: "Deep Blue",
|
||||
groups: [
|
||||
{
|
||||
label: "White / Temperature Presets",
|
||||
options: [
|
||||
{ value: "Candle", label: "Candle (#FF9329)" },
|
||||
{ value: "Tungsten 40W", label: "Tungsten 40W (#FFC58F)" },
|
||||
{ value: "Tungsten 100W", label: "Tungsten 100W (#FFD6AA)" },
|
||||
{ value: "Halogen", label: "Halogen (#FFF1E0)" },
|
||||
{ value: "Carbon Arc", label: "Carbon Arc (#FFFAF4)" },
|
||||
{ value: "High Noon Sun", label: "High Noon Sun (#FFFFFB)" },
|
||||
{ value: "Overcast Sky", label: "Overcast Sky (#C9E2FF)" },
|
||||
{ value: "Clear Blue Sky", label: "Clear Blue Sky (#409CFF)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Club / Show Presets",
|
||||
options: [
|
||||
{ value: "Deep Blue", label: "Deep Blue (#0047FF)" },
|
||||
{ value: "Electric Cyan", label: "Electric Cyan (#00D8FF)" },
|
||||
{ value: "Emerald", label: "Emerald (#00C853)" },
|
||||
{ value: "Acid Lime", label: "Acid Lime (#AEEA00)" },
|
||||
{ value: "Amber", label: "Amber (#FF9D00)" },
|
||||
{ value: "Signal Red", label: "Signal Red (#FF2D2D)" },
|
||||
{ value: "Hot Magenta", label: "Hot Magenta (#FF00A8)" },
|
||||
{ value: "Violet", label: "Violet (#7A3CFF)" },
|
||||
{ value: "Warm White", label: "Warm White (#FFD6AA)" },
|
||||
{ value: "Neutral White", label: "Neutral White (#FFF1E0)" },
|
||||
{ value: "Cool White", label: "Cool White (#C9E2FF)" },
|
||||
{ value: "Blacklight Violet", label: "Blacklight Violet (#A700FF)" },
|
||||
],
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{ value: "Laser Club", label: "Laser Club" },
|
||||
{ value: "Magenta Drive", label: "Magenta Drive" },
|
||||
{ value: "Warehouse Heat", label: "Warehouse Heat" },
|
||||
{ value: "UV Riot", label: "UV Riot" },
|
||||
{ value: "Redline", label: "Redline" },
|
||||
{ value: "Sodium Haze", label: "Sodium Haze" },
|
||||
{ value: "Afterhours", label: "Afterhours" },
|
||||
{ value: "Voltage", label: "Voltage" },
|
||||
{ value: "Candle", label: "Candle (#FF9329)" },
|
||||
{ value: "Tungsten 40W", label: "Tungsten 40W (#FFC58F)" },
|
||||
{ value: "Tungsten 100W", label: "Tungsten 100W (#FFD6AA)" },
|
||||
{ value: "Halogen", label: "Halogen (#FFF1E0)" },
|
||||
{ value: "Carbon Arc", label: "Carbon Arc (#FFFAF4)" },
|
||||
{ value: "High Noon Sun", label: "High Noon Sun (#FFFFFB)" },
|
||||
{ value: "Overcast Sky", label: "Overcast Sky (#C9E2FF)" },
|
||||
{ value: "Clear Blue Sky", label: "Clear Blue Sky (#409CFF)" },
|
||||
{ value: "Deep Blue", label: "Deep Blue (#0047FF)" },
|
||||
{ value: "Electric Cyan", label: "Electric Cyan (#00D8FF)" },
|
||||
{ value: "Emerald", label: "Emerald (#00C853)" },
|
||||
{ value: "Acid Lime", label: "Acid Lime (#AEEA00)" },
|
||||
{ value: "Amber", label: "Amber (#FF9D00)" },
|
||||
{ value: "Signal Red", label: "Signal Red (#FF2D2D)" },
|
||||
{ value: "Hot Magenta", label: "Hot Magenta (#FF00A8)" },
|
||||
{ value: "Violet", label: "Violet (#7A3CFF)" },
|
||||
{ value: "Warm White", label: "Warm White (#FFD6AA)" },
|
||||
{ value: "Neutral White", label: "Neutral White (#FFF1E0)" },
|
||||
{ value: "Cool White", label: "Cool White (#C9E2FF)" },
|
||||
{ value: "Blacklight Violet", label: "Blacklight Violet (#A700FF)" },
|
||||
],
|
||||
},
|
||||
direction: {
|
||||
@@ -171,11 +215,11 @@
|
||||
previewLayoutSignature: null,
|
||||
viewOutputSignature: null,
|
||||
stateTimer: null,
|
||||
previewTimer: null,
|
||||
lastStateRenderAt: 0,
|
||||
lastPreviewRenderAt: 0,
|
||||
stateRenderQueued: false,
|
||||
previewRenderQueued: false,
|
||||
previewAnimationFrame: 0,
|
||||
eventFilterSignature: null,
|
||||
},
|
||||
};
|
||||
@@ -225,7 +269,6 @@
|
||||
snapshotList: document.getElementById("snapshot-list"),
|
||||
motionParams: document.getElementById("motion-params"),
|
||||
colorParams: document.getElementById("color-params"),
|
||||
brightnessParams: document.getElementById("brightness-params"),
|
||||
previewGrid: document.getElementById("preview-grid"),
|
||||
summaryCards: document.getElementById("summary-cards"),
|
||||
selectedTileCard: document.getElementById("selected-tile-card"),
|
||||
@@ -565,15 +608,28 @@
|
||||
return;
|
||||
}
|
||||
apiState.ui.previewRenderQueued = true;
|
||||
const now = window.performance.now();
|
||||
const waitMs = Math.max(0, PREVIEW_RENDER_INTERVAL_MS - (now - apiState.ui.lastPreviewRenderAt));
|
||||
window.setTimeout(() => {
|
||||
const scheduleFrame = () => {
|
||||
apiState.ui.previewAnimationFrame = window.requestAnimationFrame(() => {
|
||||
const now = window.performance.now();
|
||||
if (now - apiState.ui.lastPreviewRenderAt < PREVIEW_RENDER_INTERVAL_MS) {
|
||||
scheduleFrame();
|
||||
return;
|
||||
}
|
||||
apiState.ui.previewAnimationFrame = 0;
|
||||
apiState.ui.previewRenderQueued = false;
|
||||
apiState.ui.lastPreviewRenderAt = now;
|
||||
renderPreview(false);
|
||||
renderSelectedTile();
|
||||
renderSnapshotJson();
|
||||
});
|
||||
};
|
||||
|
||||
if (apiState.ui.previewAnimationFrame) {
|
||||
apiState.ui.previewRenderQueued = false;
|
||||
apiState.ui.lastPreviewRenderAt = window.performance.now();
|
||||
renderPreview(false);
|
||||
renderSelectedTile();
|
||||
renderSnapshotJson();
|
||||
}, waitMs);
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleFrame();
|
||||
}
|
||||
|
||||
async function handlePrimitive(primitive, options) {
|
||||
@@ -1120,7 +1176,6 @@
|
||||
if (!scene || !global) {
|
||||
dom.motionParams.innerHTML = "";
|
||||
dom.colorParams.innerHTML = "";
|
||||
dom.brightnessParams.innerHTML = "";
|
||||
apiState.ui.parameterCards.clear();
|
||||
return;
|
||||
}
|
||||
@@ -1128,13 +1183,14 @@
|
||||
if (
|
||||
!force &&
|
||||
(dom.motionParams.contains(document.activeElement) ||
|
||||
dom.colorParams.contains(document.activeElement) ||
|
||||
dom.brightnessParams.contains(document.activeElement))
|
||||
dom.colorParams.contains(document.activeElement))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parameters = scene.parameters || [];
|
||||
const parameters = (scene.parameters || []).filter(
|
||||
(parameter) => !HIDDEN_PARAMETER_KEYS.has(parameter.key)
|
||||
);
|
||||
const signature = parameters
|
||||
.map((parameter) => `${parameter.key}:${parameter.kind}`)
|
||||
.join("|");
|
||||
@@ -1144,7 +1200,6 @@
|
||||
apiState.ui.parameterCards.clear();
|
||||
dom.motionParams.innerHTML = "";
|
||||
dom.colorParams.innerHTML = "";
|
||||
dom.brightnessParams.innerHTML = "";
|
||||
|
||||
parameters.forEach((parameter) => {
|
||||
const card = createParameterCard(parameter, global);
|
||||
@@ -1165,9 +1220,6 @@
|
||||
if (COLOR_PARAM_KEYS.has(parameter.key)) {
|
||||
return dom.colorParams;
|
||||
}
|
||||
if (BRIGHTNESS_PARAM_KEYS.has(parameter.key)) {
|
||||
return dom.brightnessParams;
|
||||
}
|
||||
return dom.motionParams;
|
||||
}
|
||||
|
||||
@@ -1251,13 +1303,28 @@
|
||||
card.appendChild(readout);
|
||||
} else if (controlKind === "enum") {
|
||||
primaryInput = document.createElement("select");
|
||||
const options = contract && Array.isArray(contract.options) ? contract.options : [];
|
||||
options.forEach((option) => {
|
||||
const node = document.createElement("option");
|
||||
node.value = option.value;
|
||||
node.textContent = option.label;
|
||||
primaryInput.appendChild(node);
|
||||
});
|
||||
const groupedOptions = contract && Array.isArray(contract.groups) ? contract.groups : [];
|
||||
if (groupedOptions.length) {
|
||||
groupedOptions.forEach((group) => {
|
||||
const groupNode = document.createElement("optgroup");
|
||||
groupNode.label = group.label;
|
||||
group.options.forEach((option) => {
|
||||
const node = document.createElement("option");
|
||||
node.value = option.value;
|
||||
node.textContent = option.label;
|
||||
groupNode.appendChild(node);
|
||||
});
|
||||
primaryInput.appendChild(groupNode);
|
||||
});
|
||||
} else {
|
||||
const options = contract && Array.isArray(contract.options) ? contract.options : [];
|
||||
options.forEach((option) => {
|
||||
const node = document.createElement("option");
|
||||
node.value = option.value;
|
||||
node.textContent = option.label;
|
||||
primaryInput.appendChild(node);
|
||||
});
|
||||
}
|
||||
primaryInput.addEventListener("change", async (event) => {
|
||||
const optionValue = event.target.value;
|
||||
const payloadValue =
|
||||
@@ -1550,8 +1617,8 @@
|
||||
dom.previewUpdated.textContent = `${apiState.previewResponse.generated_at_millis} ms`;
|
||||
dom.previewGrid.className = `preview-grid preview-grid-mode-${apiState.ui.previewMode}`;
|
||||
|
||||
const columnCount = uniqueNodeIds(panels).length;
|
||||
dom.previewGrid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(112px, 1fr))`;
|
||||
const columnCount = 6;
|
||||
dom.previewGrid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(0, 1fr))`;
|
||||
|
||||
const layoutSignature = panels.map((panel) => panel.key).join("|");
|
||||
if (force || layoutSignature !== apiState.ui.previewLayoutSignature) {
|
||||
@@ -2437,40 +2504,16 @@
|
||||
const sampleColors = (panel.sample_led_hex || [])
|
||||
.map((hex) => normalizeColorHex(hex))
|
||||
.filter((hex) => isHexColorString(hex));
|
||||
const palette = sampleColors.length
|
||||
? sampleColors
|
||||
: [normalizeColorHex(panel.representative_color_hex)];
|
||||
|
||||
if (palette.length === 1) {
|
||||
return TILE_LED_GEOMETRY.map(() => palette[0]);
|
||||
if (!sampleColors.length) {
|
||||
return TILE_LED_GEOMETRY.map(() => normalizeColorHex(panel.representative_color_hex));
|
||||
}
|
||||
|
||||
return TILE_LED_GEOMETRY.map((_led, index) => {
|
||||
const factor = TILE_LED_GEOMETRY.length === 1 ? 0 : index / (TILE_LED_GEOMETRY.length - 1);
|
||||
return interpolatePalette(palette, factor);
|
||||
const sourceIndex = Math.round((index / Math.max(1, TILE_LED_GEOMETRY.length - 1)) * (sampleColors.length - 1));
|
||||
return sampleColors[sourceIndex] || normalizeColorHex(panel.representative_color_hex);
|
||||
});
|
||||
}
|
||||
|
||||
function interpolatePalette(palette, factor) {
|
||||
const scaled = factor * (palette.length - 1);
|
||||
const lowerIndex = Math.floor(scaled);
|
||||
const upperIndex = Math.min(palette.length - 1, Math.ceil(scaled));
|
||||
if (lowerIndex === upperIndex) {
|
||||
return palette[lowerIndex];
|
||||
}
|
||||
return mixHexColors(palette[lowerIndex], palette[upperIndex], scaled - lowerIndex);
|
||||
}
|
||||
|
||||
function mixHexColors(leftHex, rightHex, factor) {
|
||||
const left = parseHexColor(leftHex);
|
||||
const right = parseHexColor(rightHex);
|
||||
return rgbToHex(
|
||||
Math.round(lerp(left.r, right.r, factor)),
|
||||
Math.round(lerp(left.g, right.g, factor)),
|
||||
Math.round(lerp(left.b, right.b, factor))
|
||||
);
|
||||
}
|
||||
|
||||
function parseHexColor(hex) {
|
||||
const normalized = normalizeColorHex(hex).slice(1);
|
||||
return {
|
||||
@@ -2486,9 +2529,10 @@
|
||||
|
||||
function previewLedOpacity(energyPercent) {
|
||||
if (!energyPercent) {
|
||||
return "0.18";
|
||||
return "0.1";
|
||||
}
|
||||
return String(Math.min(1, 0.42 + energyPercent / 160));
|
||||
const steppedPercent = Math.round(Math.max(0, Math.min(100, energyPercent)) / 10) * 10;
|
||||
return String(Math.max(0.1, steppedPercent / 100));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="toolbar-control toolbar-control-inline">
|
||||
<span class="toolbar-label">Master</span>
|
||||
<input id="brightness-slider" type="range" min="0" max="1" step="0.01" />
|
||||
<strong id="brightness-value">0%</strong>
|
||||
</label>
|
||||
|
||||
<button id="go-button" class="toolbar-button" type="button">Go</button>
|
||||
<button id="fade-go-button" class="toolbar-button toolbar-button-primary" type="button">
|
||||
Fade Go
|
||||
@@ -83,18 +89,6 @@
|
||||
<div id="color-params" class="parameter-stack"></div>
|
||||
</section>
|
||||
|
||||
<section class="dock-section">
|
||||
<div class="dock-header">
|
||||
<h2>Brightness</h2>
|
||||
<p>Global intensity plus pattern-level brightness controls.</p>
|
||||
</div>
|
||||
<label class="control-field">
|
||||
<span>Master Brightness</span>
|
||||
<input id="brightness-slider" type="range" min="0" max="1" step="0.01" />
|
||||
<strong id="brightness-value">0%</strong>
|
||||
</label>
|
||||
<div id="brightness-params" class="parameter-stack"></div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section class="workspace-stage">
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #1e1e1e;
|
||||
--bg-elevated: #252526;
|
||||
--bg-elevated-2: #2d2d30;
|
||||
--bg: #151920;
|
||||
--bg-elevated: #1b212c;
|
||||
--bg-elevated-2: #202838;
|
||||
--bg-stage: #0d1016;
|
||||
--bg-stage-2: #11151d;
|
||||
--line: #3c3c3c;
|
||||
--line-soft: #2f2f33;
|
||||
--text: #cccccc;
|
||||
--line: rgba(182, 202, 227, 0.14);
|
||||
--line-soft: rgba(182, 202, 227, 0.08);
|
||||
--text: #c9d2df;
|
||||
--text-strong: #f3f6fb;
|
||||
--muted: #8f99a5;
|
||||
--muted: #95a1b2;
|
||||
--accent: #007acc;
|
||||
--accent-strong: #094771;
|
||||
--accent-strong: #0a4f8d;
|
||||
--accent-soft: rgba(0, 122, 204, 0.18);
|
||||
--warning: #d6a04d;
|
||||
--danger: #c63b1e;
|
||||
--danger-soft: rgba(198, 59, 30, 0.18);
|
||||
--success: #1f8b63;
|
||||
--success-soft: rgba(31, 139, 99, 0.18);
|
||||
--shadow: 0 18px 48px rgba(0, 0, 0, 0.34);
|
||||
--tile-shadow: 0 14px 34px rgba(0, 0, 0, 0.38);
|
||||
--shadow: 0 10px 26px rgba(0, 0, 0, 0.2);
|
||||
--tile-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
|
||||
--radius-sm: 3px;
|
||||
--radius-md: 4px;
|
||||
--radius-lg: 6px;
|
||||
@@ -110,26 +110,26 @@ input[type="range"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 6px 10px;
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
.topbar-creative .topbar-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.topbar-creative .toolbar-control,
|
||||
.topbar-creative .toolbar-group,
|
||||
.topbar-creative .toolbar-button {
|
||||
min-height: 30px;
|
||||
padding: 4px 8px;
|
||||
min-height: 26px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.topbar-creative .toolbar-control-fade {
|
||||
min-width: 222px;
|
||||
min-width: 184px;
|
||||
}
|
||||
|
||||
.topbar-brand,
|
||||
@@ -166,10 +166,10 @@ input[type="range"] {
|
||||
.toolbar-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
padding: 6px 8px;
|
||||
background: #1f1f1f;
|
||||
gap: 6px;
|
||||
min-height: 28px;
|
||||
padding: 3px 6px;
|
||||
background: rgba(17, 22, 31, 0.72);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
@@ -178,13 +178,13 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.toolbar-control-fade {
|
||||
min-width: 250px;
|
||||
min-width: 192px;
|
||||
}
|
||||
|
||||
#tempo-bpm-input,
|
||||
#transition-seconds-input {
|
||||
width: 82px;
|
||||
min-width: 82px;
|
||||
width: 68px;
|
||||
min-width: 68px;
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
@@ -198,10 +198,10 @@ input[type="range"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 100px;
|
||||
min-height: 24px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.77rem;
|
||||
min-width: 88px;
|
||||
min-height: 22px;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
@@ -245,9 +245,9 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
min-height: 34px;
|
||||
padding: 7px 14px;
|
||||
background: var(--bg-elevated-2);
|
||||
min-height: 28px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(17, 22, 31, 0.74);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
@@ -287,9 +287,9 @@ a.toolbar-button {
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 270px minmax(0, 1fr) 310px;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
grid-template-columns: 252px minmax(0, 1fr) 236px;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ a.toolbar-button {
|
||||
|
||||
.workspace-rail {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
align-content: start;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
@@ -308,27 +308,29 @@ a.toolbar-button {
|
||||
|
||||
.workspace-stage {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) 220px;
|
||||
gap: 8px;
|
||||
grid-template-rows: minmax(0, 1fr) 148px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stage-panel,
|
||||
.dock-section {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dock-section {
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
background: color-mix(in srgb, var(--bg-elevated) 86%, transparent);
|
||||
}
|
||||
|
||||
.stage-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
min-height: 0;
|
||||
background: color-mix(in srgb, var(--bg-elevated-2) 74%, transparent);
|
||||
}
|
||||
|
||||
.stage-panel-preview {
|
||||
@@ -343,14 +345,14 @@ a.toolbar-button {
|
||||
.stage-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dock-header h2,
|
||||
.stage-header h2 {
|
||||
margin: 0;
|
||||
font-size: 0.96rem;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
@@ -361,8 +363,8 @@ a.toolbar-button {
|
||||
.panel-meta,
|
||||
.info-detail,
|
||||
.event-meta {
|
||||
margin: 3px 0 0;
|
||||
font-size: 0.78rem;
|
||||
margin: 1px 0 0;
|
||||
font-size: 0.68rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -385,12 +387,12 @@ a.toolbar-button {
|
||||
.compact-form-two,
|
||||
.parameter-card label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.control-field span,
|
||||
.parameter-card span {
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -408,13 +410,13 @@ a.toolbar-button {
|
||||
.summary-cards,
|
||||
.info-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.button-stack,
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -423,7 +425,7 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.compact-form {
|
||||
margin-top: 10px;
|
||||
margin-top: 8px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -433,7 +435,7 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.compact-form-two {
|
||||
margin-top: 10px;
|
||||
margin-top: 8px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
}
|
||||
@@ -442,7 +444,7 @@ a.toolbar-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
min-height: 30px;
|
||||
padding: 0 6px;
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -450,28 +452,28 @@ a.toolbar-button {
|
||||
.pending-status-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mini-status {
|
||||
padding: 8px 10px;
|
||||
padding: 6px 8px;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.preview-stage {
|
||||
min-height: 0;
|
||||
padding: 10px 10px;
|
||||
padding: 4px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 14%),
|
||||
linear-gradient(180deg, var(--bg-stage) 0%, var(--bg-stage-2) 100%);
|
||||
border: 1px solid #20252f;
|
||||
border: 1px solid rgba(182, 202, 227, 0.08);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 4px;
|
||||
min-height: 100%;
|
||||
align-content: center;
|
||||
}
|
||||
@@ -479,12 +481,13 @@ a.toolbar-button {
|
||||
.preview-tile {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: 168px;
|
||||
padding: 8px;
|
||||
background: linear-gradient(180deg, #1b1b1b 0%, #111318 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
aspect-ratio: 1 / 1;
|
||||
min-height: 0;
|
||||
padding: 4px;
|
||||
background: linear-gradient(180deg, #151a24 0%, #10141d 100%);
|
||||
border: 1px solid rgba(182, 202, 227, 0.12);
|
||||
color: var(--text-strong);
|
||||
box-shadow: var(--tile-shadow);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -492,11 +495,11 @@ a.toolbar-button {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: auto -15% -40% auto;
|
||||
width: 62%;
|
||||
height: 62%;
|
||||
width: 52%;
|
||||
height: 52%;
|
||||
background: radial-gradient(circle, var(--tile-glow, #4d7cff), transparent 72%);
|
||||
pointer-events: none;
|
||||
opacity: 0.16;
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.preview-tile:hover {
|
||||
@@ -504,9 +507,9 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.preview-tile.is-selected {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
border-color: rgba(0, 122, 204, 0.7);
|
||||
outline: 1px solid var(--accent);
|
||||
outline-offset: -1px;
|
||||
border-color: rgba(0, 122, 204, 0.5);
|
||||
}
|
||||
|
||||
.preview-tile.is-panel-test {
|
||||
@@ -520,12 +523,12 @@ a.toolbar-button {
|
||||
.tile-preview-shell {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 148px;
|
||||
border-radius: 4px;
|
||||
min-height: 0;
|
||||
border-radius: 3px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 16%),
|
||||
linear-gradient(180deg, #060912 0%, #080d18 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(182, 202, 227, 0.08);
|
||||
}
|
||||
|
||||
.tile-led-ring {
|
||||
@@ -535,10 +538,10 @@ a.toolbar-button {
|
||||
|
||||
.tile-led {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
margin-left: -2px;
|
||||
margin-top: -2px;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
margin-left: -1.5px;
|
||||
margin-top: -1.5px;
|
||||
border-radius: 999px;
|
||||
background: var(--tile-color, #4d7cff);
|
||||
opacity: var(--led-opacity, 0.85);
|
||||
@@ -546,16 +549,16 @@ a.toolbar-button {
|
||||
|
||||
.tile-overlay {
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
inset: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 14px 10px 12px;
|
||||
padding: 5px 4px 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tile-label {
|
||||
font-size: clamp(1.15rem, 1vw + 0.8rem, 2rem);
|
||||
font-size: clamp(0.72rem, 0.32vw + 0.62rem, 1rem);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: rgba(237, 243, 255, 0.94);
|
||||
@@ -563,7 +566,7 @@ a.toolbar-button {
|
||||
|
||||
.tile-caption {
|
||||
margin-top: auto;
|
||||
font-size: 1rem;
|
||||
font-size: 0.68rem;
|
||||
color: rgba(214, 224, 238, 0.76);
|
||||
}
|
||||
|
||||
@@ -766,9 +769,9 @@ a.toolbar-button {
|
||||
.tile-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
font-size: 0.74rem;
|
||||
font-size: 0.58rem;
|
||||
color: rgba(214, 224, 238, 0.64);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
@@ -803,10 +806,10 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.preview-grid-mode-leds .tile-led {
|
||||
width: 4.6px;
|
||||
height: 4.6px;
|
||||
margin-left: -2.3px;
|
||||
margin-top: -2.3px;
|
||||
width: 3.4px;
|
||||
height: 3.4px;
|
||||
margin-left: -1.7px;
|
||||
margin-top: -1.7px;
|
||||
}
|
||||
|
||||
.preview-grid-mode-tile .tile-preview-shell {
|
||||
@@ -826,9 +829,9 @@ a.toolbar-button {
|
||||
.event-item,
|
||||
.selected-tile-card,
|
||||
.info-row {
|
||||
padding: 10px;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid var(--line);
|
||||
padding: 6px;
|
||||
background: rgba(17, 22, 31, 0.7);
|
||||
border: 1px solid rgba(182, 202, 227, 0.1);
|
||||
}
|
||||
|
||||
.summary-card span {
|
||||
@@ -861,7 +864,7 @@ a.toolbar-button {
|
||||
|
||||
.list-item-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.76rem;
|
||||
color: var(--muted);
|
||||
@@ -879,7 +882,7 @@ a.toolbar-button {
|
||||
|
||||
.parameter-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.parameter-card.is-staged {
|
||||
@@ -891,12 +894,20 @@ a.toolbar-button {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.parameter-key {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.parameter-card select,
|
||||
.parameter-card input[type="text"],
|
||||
.parameter-card input[type="number"] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.parameter-readout {
|
||||
@@ -906,13 +917,13 @@ a.toolbar-button {
|
||||
|
||||
.color-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr);
|
||||
grid-template-columns: 50px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-input-row input[type="color"] {
|
||||
width: 44px;
|
||||
width: 50px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
@@ -921,11 +932,11 @@ a.toolbar-button {
|
||||
|
||||
.selected-tile-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.selected-tile-title {
|
||||
font-size: 1.08rem;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
@@ -933,13 +944,13 @@ a.toolbar-button {
|
||||
.selected-tile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
@@ -959,7 +970,7 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.pending-card {
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
background: rgba(214, 160, 77, 0.08);
|
||||
border: 1px solid rgba(214, 160, 77, 0.2);
|
||||
}
|
||||
@@ -971,7 +982,7 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.primitive-error-banner {
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
background: var(--danger-soft);
|
||||
border: 1px solid rgba(198, 59, 30, 0.4);
|
||||
color: #ffb09e;
|
||||
@@ -984,8 +995,8 @@ a.toolbar-button {
|
||||
|
||||
.event-filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 170px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
grid-template-columns: 132px minmax(0, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.event-list {
|
||||
@@ -995,7 +1006,7 @@ a.toolbar-button {
|
||||
|
||||
.event-item {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.event-item strong {
|
||||
@@ -1038,9 +1049,9 @@ a.toolbar-button {
|
||||
|
||||
.snapshot-json {
|
||||
margin: 8px 0 0;
|
||||
max-height: 260px;
|
||||
max-height: 170px;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
background: #111317;
|
||||
border: 1px solid #20252f;
|
||||
color: #dde7f5;
|
||||
@@ -1053,8 +1064,8 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 12px;
|
||||
border: 1px dashed var(--line);
|
||||
padding: 8px;
|
||||
border: 1px dashed rgba(182, 202, 227, 0.2);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -1062,13 +1073,18 @@ a.toolbar-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
select optgroup {
|
||||
background: #141923;
|
||||
color: #cdd7e5;
|
||||
}
|
||||
|
||||
@media (max-width: 1460px) {
|
||||
.topbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
grid-template-columns: 248px minmax(0, 1fr) 280px;
|
||||
grid-template-columns: 192px minmax(0, 1fr) 212px;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
@@ -1091,7 +1107,7 @@ a.toolbar-button {
|
||||
}
|
||||
|
||||
.workspace-stage {
|
||||
grid-template-rows: minmax(480px, auto) auto;
|
||||
grid-template-rows: minmax(360px, auto) auto;
|
||||
}
|
||||
|
||||
.workspace-rail,
|
||||
|
||||
@@ -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