feat: add native runtime settings
This commit is contained in:
@@ -20,6 +20,39 @@
|
||||
const movingAverage5 = document.getElementById("moving-average-5");
|
||||
const movingAverage20 = document.getElementById("moving-average-20");
|
||||
const status = document.getElementById("operator-status");
|
||||
const operatorSettingsMessage = document.getElementById("operator-settings-message");
|
||||
const operatorSettingsRestart = document.getElementById("operator-settings-restart");
|
||||
const operatorNavigationExpanded = document.getElementById("operator-navigation-expanded");
|
||||
const operatorSettingsAppVersion = document.getElementById("operator-settings-app-version");
|
||||
const operatorSettingsPlayoutMode = document.getElementById("operator-settings-playout-mode");
|
||||
const operatorSettingsPlayoutConnection = document.getElementById(
|
||||
"operator-settings-playout-connection");
|
||||
const operatorSettingsOracleState = document.getElementById("operator-settings-oracle-state");
|
||||
const operatorSettingsMariaState = document.getElementById("operator-settings-maria-state");
|
||||
const operatorSettingsDbMonitor = document.getElementById("operator-settings-db-monitor");
|
||||
const operatorFolderControls = Object.freeze({
|
||||
design: Object.freeze({
|
||||
name: document.getElementById("operator-design-folder-name"),
|
||||
status: document.getElementById("operator-design-folder-status"),
|
||||
source: document.getElementById("operator-design-folder-source"),
|
||||
select: document.getElementById("operator-design-folder-select"),
|
||||
reset: document.getElementById("operator-design-folder-reset")
|
||||
}),
|
||||
resource: Object.freeze({
|
||||
name: document.getElementById("operator-resource-folder-name"),
|
||||
status: document.getElementById("operator-resource-folder-status"),
|
||||
source: document.getElementById("operator-resource-folder-source"),
|
||||
select: document.getElementById("operator-resource-folder-select"),
|
||||
reset: document.getElementById("operator-resource-folder-reset")
|
||||
}),
|
||||
background: Object.freeze({
|
||||
name: document.getElementById("operator-background-folder-name"),
|
||||
status: document.getElementById("operator-background-folder-status"),
|
||||
source: document.getElementById("operator-background-folder-source"),
|
||||
select: document.getElementById("operator-background-folder-select"),
|
||||
reset: document.getElementById("operator-background-folder-reset")
|
||||
})
|
||||
});
|
||||
const dialog = document.getElementById("legacy-dialog");
|
||||
const dialogCaption = document.getElementById("legacy-dialog-caption");
|
||||
const dialogMessage = document.getElementById("legacy-dialog-message");
|
||||
@@ -156,6 +189,11 @@
|
||||
let manualFinancialRenderStateKey = "";
|
||||
let manualListRenderStateKey = "";
|
||||
let operatorCatalogRenderStateKey = "";
|
||||
let operatorSettingsRenderRevision = null;
|
||||
let operatorSettingsRevisionInitialized = false;
|
||||
let pendingOperatorSettingsFocusId = null;
|
||||
let pendingOperatorSettingsFallbackFocusId = null;
|
||||
let operatorSettingsWasBusy = false;
|
||||
let renderedTreeTabId = null;
|
||||
let treeInteractionLocked = false;
|
||||
let treeFocusOwnedBeforeRender = false;
|
||||
@@ -3681,6 +3719,167 @@
|
||||
}
|
||||
}
|
||||
|
||||
function operatorSettingsText(value, fallback) {
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
if (value && typeof value === "object") {
|
||||
const candidate = value.displayName || value.label || value.status || value.message;
|
||||
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function operatorDiagnosticTone(value, label) {
|
||||
if (value && typeof value === "object") {
|
||||
if (value.isConnected === true || value.isHealthy === true) return "good";
|
||||
if (value.isConnected === false || value.isHealthy === false) return "bad";
|
||||
}
|
||||
const normalized = String(label || "").toLowerCase();
|
||||
if (/연결됨|정상|사용 가능|ready|connected|healthy/.test(normalized)) return "good";
|
||||
if (/확인 중|재연결|대기|dry run|감지됨|unknown|waiting/.test(normalized)) {
|
||||
return "warning";
|
||||
}
|
||||
if (/끊김|미연결|오류|실패|불명확|disconnected|fault|error|failed/.test(normalized)) {
|
||||
return "bad";
|
||||
}
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
function setOperatorDiagnostic(element, value, fallback) {
|
||||
const label = operatorSettingsText(value, fallback);
|
||||
if (element.textContent !== label) element.textContent = label;
|
||||
const tone = operatorDiagnosticTone(value, label);
|
||||
if (tone === "neutral") element.removeAttribute("data-state");
|
||||
else element.dataset.state = tone;
|
||||
}
|
||||
|
||||
function operatorPlayoutModeLabel(playout) {
|
||||
if (!playout) return "확인 중";
|
||||
if (playout.mode === "live") return "LIVE";
|
||||
if (playout.mode === "dryRun") return "DRY RUN";
|
||||
if (playout.mode === "disabled") return "송출 비활성";
|
||||
return operatorSettingsText(playout.mode, "확인 중");
|
||||
}
|
||||
|
||||
function operatorPlayoutConnectionLabel(playout) {
|
||||
if (!playout) return "확인 중";
|
||||
if (playout.outcomeUnknown === true) return "결과 불명확";
|
||||
if (playout.mode === "disabled") return "송출 비활성";
|
||||
if (playout.mode === "dryRun") return "DRY RUN 준비됨";
|
||||
if (playout.isConnected === true) return "Tornado2 연결됨";
|
||||
if (playout.isProcessRunning === true) return "Tornado2 감지됨";
|
||||
return "미연결";
|
||||
}
|
||||
|
||||
function renderOperatorFolder(kind, folder, isBusy) {
|
||||
const controls = operatorFolderControls[kind];
|
||||
const current = folder && typeof folder === "object" ? folder : {};
|
||||
const defaultNames = {
|
||||
design: "앱 기본 디자인 폴더",
|
||||
resource: "앱 기본 설정 폴더",
|
||||
background: "앱 기본 배경 폴더"
|
||||
};
|
||||
const displayName = operatorSettingsText(current.displayName, defaultNames[kind]);
|
||||
if (controls.name.textContent !== displayName) controls.name.textContent = displayName;
|
||||
controls.name.title = displayName;
|
||||
|
||||
const valid = current.isValid === true;
|
||||
const invalid = current.isValid === false;
|
||||
const statusLabel = operatorSettingsText(
|
||||
current.status,
|
||||
valid ? "사용 가능" : (invalid ? "확인 필요" : "확인 중"));
|
||||
if (controls.status.textContent !== statusLabel) controls.status.textContent = statusLabel;
|
||||
controls.status.classList.toggle("valid", valid);
|
||||
controls.status.classList.toggle("invalid", invalid);
|
||||
controls.status.classList.toggle("neutral", !valid && !invalid);
|
||||
controls.source.textContent = current.isCustom === true ? "사용자 지정" : "앱 기본값";
|
||||
controls.select.disabled = isBusy;
|
||||
controls.reset.disabled = isBusy || current.isCustom !== true;
|
||||
}
|
||||
|
||||
function renderOperatorSettings(state) {
|
||||
const settings = state && state.operatorSettings;
|
||||
const isBusy = !settings || state.isBusy === true;
|
||||
const wasBusy = operatorSettingsWasBusy;
|
||||
let revisionChanged = false;
|
||||
renderOperatorFolder("design", settings && settings.design, isBusy);
|
||||
renderOperatorFolder("resource", settings && settings.resource, isBusy);
|
||||
renderOperatorFolder("background", settings && settings.background, isBusy);
|
||||
|
||||
operatorNavigationExpanded.disabled = isBusy;
|
||||
if (settings) {
|
||||
const revision = settings.revision === undefined ? null : settings.revision;
|
||||
revisionChanged = !operatorSettingsRevisionInitialized ||
|
||||
revision !== operatorSettingsRenderRevision;
|
||||
if (revisionChanged) {
|
||||
const expanded = settings.navigationExpanded !== false;
|
||||
operatorNavigationExpanded.checked = expanded;
|
||||
if (window.LegacyWorkspaceNavigation &&
|
||||
window.LegacyWorkspaceNavigation.expanded() !== expanded) {
|
||||
window.LegacyWorkspaceNavigation.setExpanded(expanded);
|
||||
}
|
||||
operatorSettingsRenderRevision = revision;
|
||||
operatorSettingsRevisionInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
const message = operatorSettingsText(settings && settings.message, "");
|
||||
operatorSettingsMessage.hidden = !message;
|
||||
if (operatorSettingsMessage.textContent !== message) {
|
||||
operatorSettingsMessage.textContent = message;
|
||||
}
|
||||
const messageKind = settings && typeof settings.messageKind === "string"
|
||||
? settings.messageKind.toLowerCase()
|
||||
: "";
|
||||
operatorSettingsMessage.className = "settings-message" +
|
||||
(["information", "success", "warning", "error"].includes(messageKind)
|
||||
? " " + messageKind
|
||||
: "");
|
||||
operatorSettingsRestart.hidden = !(settings && settings.restartRequired === true);
|
||||
|
||||
setOperatorDiagnostic(
|
||||
operatorSettingsAppVersion,
|
||||
settings && settings.appVersion,
|
||||
"확인 중");
|
||||
const playout = state && state.playout;
|
||||
setOperatorDiagnostic(operatorSettingsPlayoutMode, operatorPlayoutModeLabel(playout), "확인 중");
|
||||
setOperatorDiagnostic(
|
||||
operatorSettingsPlayoutConnection,
|
||||
operatorPlayoutConnectionLabel(playout),
|
||||
"확인 중");
|
||||
setOperatorDiagnostic(
|
||||
operatorSettingsOracleState,
|
||||
settings && settings.oracleState,
|
||||
"확인 중");
|
||||
setOperatorDiagnostic(
|
||||
operatorSettingsMariaState,
|
||||
settings && settings.mariaState,
|
||||
"확인 중");
|
||||
const pollingSeconds = Number(settings && settings.databasePollingSeconds);
|
||||
operatorSettingsDbMonitor.textContent = Number.isFinite(pollingSeconds) && pollingSeconds > 0
|
||||
? String(pollingSeconds) + "초마다"
|
||||
: "10초마다";
|
||||
|
||||
operatorSettingsWasBusy = isBusy;
|
||||
if (pendingOperatorSettingsFocusId && !isBusy && (wasBusy || revisionChanged)) {
|
||||
const focusId = pendingOperatorSettingsFocusId;
|
||||
const fallbackFocusId = pendingOperatorSettingsFallbackFocusId;
|
||||
pendingOperatorSettingsFocusId = null;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
window.requestAnimationFrame(function () {
|
||||
const primary = document.getElementById(focusId);
|
||||
const fallback = fallbackFocusId
|
||||
? document.getElementById(fallbackFocusId)
|
||||
: null;
|
||||
const target = primary && !primary.disabled ? primary : fallback;
|
||||
const active = document.activeElement;
|
||||
if (target && !target.disabled && !target.closest("[inert]") &&
|
||||
(active === document.body || active === null)) {
|
||||
target.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function render(state) {
|
||||
const isBusy = state.isBusy === true;
|
||||
const treeTabId = beginTreeRender(state);
|
||||
@@ -3711,6 +3910,7 @@
|
||||
renderManualFinancial(state);
|
||||
renderManualLists(state);
|
||||
renderOperatorCatalog(state);
|
||||
renderOperatorSettings(state);
|
||||
renderPlaylist(state);
|
||||
renderNamedPlaylist(state);
|
||||
renderDialog(state);
|
||||
@@ -3718,6 +3918,42 @@
|
||||
status.textContent = isBusy ? "종목을 검색하고 있습니다." : (state.statusMessage || "");
|
||||
}
|
||||
|
||||
Object.keys(operatorFolderControls).forEach(function (kind) {
|
||||
const controls = operatorFolderControls[kind];
|
||||
controls.select.addEventListener("click", function () {
|
||||
pendingOperatorSettingsFocusId = controls.select.id;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
if (!send("choose-operator-folder", { kind: kind })) {
|
||||
pendingOperatorSettingsFocusId = null;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
}
|
||||
});
|
||||
controls.reset.addEventListener("click", function () {
|
||||
pendingOperatorSettingsFocusId = controls.reset.id;
|
||||
pendingOperatorSettingsFallbackFocusId = controls.select.id;
|
||||
if (!send("reset-operator-folder", { kind: kind })) {
|
||||
pendingOperatorSettingsFocusId = null;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
operatorNavigationExpanded.addEventListener("change", function () {
|
||||
const expanded = operatorNavigationExpanded.checked === true;
|
||||
pendingOperatorSettingsFocusId = operatorNavigationExpanded.id;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
if (!send("set-operator-navigation-expanded", { expanded: expanded })) {
|
||||
pendingOperatorSettingsFocusId = null;
|
||||
pendingOperatorSettingsFallbackFocusId = null;
|
||||
operatorNavigationExpanded.checked = !expanded;
|
||||
return;
|
||||
}
|
||||
if (window.LegacyWorkspaceNavigation &&
|
||||
window.LegacyWorkspaceNavigation.expanded() !== expanded) {
|
||||
window.LegacyWorkspaceNavigation.setExpanded(expanded);
|
||||
}
|
||||
});
|
||||
|
||||
searchButton.addEventListener("click", function () {
|
||||
send("search-stocks", { text: searchInput.value, trigger: "button" });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user