brighter-trading/src/static/signals.js

874 lines
30 KiB
JavaScript

/**
* SigUIManager - Handles DOM updates and signal card rendering
*/
class SigUIManager {
constructor() {
this.targetEl = null;
this.formElement = null;
this.onDeleteSignal = null;
}
/**
* Initializes the UI elements with provided IDs.
* @param {string} targetId - The ID of the HTML element where signals will be displayed.
* @param {string} formElId - The ID of the HTML element for the signal creation form.
*/
initUI(targetId, formElId) {
this.targetEl = document.getElementById(targetId);
if (!this.targetEl) {
console.warn(`Element for displaying signals "${targetId}" not found.`);
}
this.formElement = document.getElementById(formElId);
if (!this.formElement) {
console.warn(`Signals form element "${formElId}" not found.`);
}
}
/**
* Displays the form for creating or editing a signal.
* @param {string} action - The action to perform ('new' or 'edit').
* @param {object|null} signalData - The data of the signal to edit (only applicable for 'edit' action).
*/
displayForm(action, signalData = null) {
if (!this.formElement) {
console.error("Form element not initialized.");
return;
}
const headerTitle = this.formElement.querySelector("h1");
const submitCreateBtn = this.formElement.querySelector("#submit-create-signal");
const submitEditBtn = this.formElement.querySelector("#submit-edit-signal");
const nameBox = this.formElement.querySelector('#signal_name');
const publicCheckbox = this.formElement.querySelector('#signal_public_checkbox');
const tblKeyInput = this.formElement.querySelector('#signal_tbl_key');
// Reset form to panel 1
const panel1 = this.formElement.querySelector('#panel_1');
const panel2 = this.formElement.querySelector('#panel_2');
const panel3 = this.formElement.querySelector('#panel_3');
if (panel1) panel1.style.display = 'grid';
if (panel2) panel2.style.display = 'none';
if (panel3) panel3.style.display = 'none';
if (action === 'new') {
if (headerTitle) headerTitle.textContent = "Add New Signal";
if (submitCreateBtn) submitCreateBtn.style.display = "inline-block";
if (submitEditBtn) submitEditBtn.style.display = "none";
if (nameBox) nameBox.value = '';
if (publicCheckbox) publicCheckbox.checked = false;
if (tblKeyInput) tblKeyInput.value = '';
} else if (action === 'edit' && signalData) {
if (headerTitle) headerTitle.textContent = "Edit Signal";
if (submitCreateBtn) submitCreateBtn.style.display = "none";
if (submitEditBtn) submitEditBtn.style.display = "inline-block";
if (nameBox) nameBox.value = signalData.name || '';
if (publicCheckbox) publicCheckbox.checked = !!signalData.public;
if (tblKeyInput) tblKeyInput.value = signalData.tbl_key || '';
// Pre-fill source fields
const sigSource = this.formElement.querySelector('#sig_source');
const sigProp = this.formElement.querySelector('#sig_prop');
const sig2Source = this.formElement.querySelector('#sig2_source');
const sig2Prop = this.formElement.querySelector('#sig2_prop');
const sigType = this.formElement.querySelector('#select_s_type');
const valueInput = this.formElement.querySelector('#value');
if (sigSource && signalData.source1) sigSource.value = signalData.source1;
if (sigProp && signalData.prop1) {
// Fill prop options first, then set value
if (UI.signals && signalData.source1) {
UI.signals.fill_prop('sig_prop', signalData.source1);
}
setTimeout(() => { if (sigProp) sigProp.value = signalData.prop1; }, 50);
}
// Handle source2 - could be 'value' for fixed value type
if (signalData.source2 === 'value') {
if (sigType) sigType.value = 'Value';
if (valueInput) valueInput.value = signalData.prop2 || '';
} else {
if (sigType) sigType.value = 'Comparison';
if (sig2Source && signalData.source2) sig2Source.value = signalData.source2;
if (sig2Prop && signalData.prop2) {
if (UI.signals && signalData.source2) {
UI.signals.fill_prop('sig2_prop', signalData.source2);
}
setTimeout(() => { if (sig2Prop) sig2Prop.value = signalData.prop2; }, 50);
}
}
// Set operator
const operatorRadios = this.formElement.querySelectorAll('input[name="Operator"]');
operatorRadios.forEach(radio => {
radio.checked = radio.value === signalData.operator;
});
// Set range if applicable
if (signalData.operator === '+/-' && signalData.range) {
const rangeVal = this.formElement.querySelector('#rangeVal');
const rangeSlider = this.formElement.querySelector('#rangeSlider');
if (rangeVal) rangeVal.value = signalData.range;
if (rangeSlider) rangeSlider.value = signalData.range;
}
}
this.formElement.style.display = "grid";
}
/**
* Hides the signal form.
*/
hideForm() {
if (this.formElement) {
this.formElement.style.display = 'none';
}
}
/**
* Updates the HTML representation of the signals as cards.
* @param {Object[]} signals - The list of signals to display.
*/
updateSignalsHtml(signals) {
if (!this.targetEl) {
console.error("Target element for displaying signals is not set.");
return;
}
// Clear existing content
while (this.targetEl.firstChild) {
this.targetEl.removeChild(this.targetEl.firstChild);
}
// Create and append new elements for all signals
for (const signal of signals) {
try {
const signalCard = this._createSignalCard(signal);
this.targetEl.appendChild(signalCard);
} catch (error) {
console.error(`Error processing signal:`, error, signal);
}
}
}
/**
* Creates a signal card HTML element.
* @param {Object} signal - The signal data.
* @returns {HTMLElement} - The card element.
*/
_createSignalCard(signal) {
const signalItem = document.createElement('div');
signalItem.className = 'signal-item';
signalItem.setAttribute('data-signal-id', signal.tbl_key || signal.name);
// Add state-based styling
const isTrue = signal.state === true || signal.state === 'true' || signal.state === 1;
if (isTrue) {
signalItem.classList.add('signal-true');
} else {
signalItem.classList.add('signal-false');
}
// Delete button
const deleteButton = document.createElement('button');
deleteButton.className = 'delete-button';
deleteButton.innerHTML = '✘';
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
if (this.onDeleteSignal) {
this.onDeleteSignal(signal.tbl_key || signal.name);
}
});
signalItem.appendChild(deleteButton);
// Signal icon container
const signalIcon = document.createElement('div');
signalIcon.className = 'signal-icon';
signalIcon.addEventListener('click', () => {
// Open edit form when clicking on signal
this.displayForm('edit', signal);
});
// Signal name
const signalName = document.createElement('div');
signalName.className = 'signal-name';
signalName.textContent = signal.name || 'Unnamed Signal';
signalIcon.appendChild(signalName);
// State indicator
const stateIndicator = document.createElement('div');
stateIndicator.className = 'signal-state';
stateIndicator.id = `${signal.name}_state`;
stateIndicator.textContent = isTrue ? 'TRUE' : 'FALSE';
signalIcon.appendChild(stateIndicator);
signalItem.appendChild(signalIcon);
// Hover details panel
const signalHover = document.createElement('div');
signalHover.className = 'signal-hover';
// Build hover content
let hoverHtml = `<strong>${signal.name || 'Unnamed Signal'}</strong>`;
hoverHtml += `<div class="signal-details">`;
hoverHtml += `<span>Source 1: ${signal.source1} (${signal.prop1})</span>`;
hoverHtml += `<span id="${signal.name}_value1">Value: ${signal.value1 ?? signal.last_value1 ?? 'N/A'}</span>`;
hoverHtml += `<span>Operator: ${signal.operator}${signal.operator === '+/-' ? ` (range: ${signal.range})` : ''}</span>`;
if (signal.source2 === 'value') {
hoverHtml += `<span>Compare to: ${signal.prop2}</span>`;
} else {
hoverHtml += `<span>Source 2: ${signal.source2} (${signal.prop2})</span>`;
hoverHtml += `<span id="${signal.name}_value2">Value: ${signal.value2 ?? signal.last_value2 ?? 'N/A'}</span>`;
}
// State display
const stateClass = isTrue ? 'state-true' : 'state-false';
hoverHtml += `<span class="${stateClass}">State: ${isTrue ? 'TRUE' : 'FALSE'}</span>`;
if (signal.public) {
hoverHtml += `<span class="signal-public-badge">Public</span>`;
}
hoverHtml += `</div>`;
signalHover.innerHTML = hoverHtml;
signalItem.appendChild(signalHover);
return signalItem;
}
/**
* Updates a single signal's state display.
* @param {string} signalName - The name of the signal.
* @param {boolean} state - The new state.
*/
updateSignalState(signalName, state) {
const stateEl = document.getElementById(`${signalName}_state`);
if (stateEl) {
const isTrue = state === true || state === 'true' || state === 1;
stateEl.textContent = isTrue ? 'TRUE' : 'FALSE';
// Update parent card styling
const card = stateEl.closest('.signal-item');
if (card) {
card.classList.remove('signal-true', 'signal-false');
card.classList.add(isTrue ? 'signal-true' : 'signal-false');
}
}
}
/**
* Sets the callback function for deleting a signal.
* @param {Function} callback - The callback function.
*/
registerDeleteSignalCallback(callback) {
this.onDeleteSignal = callback;
}
}
/**
* SigDataManager - Manages in-memory signal data store
*/
class SigDataManager {
constructor() {
this.signals = [];
}
/**
* Fetches the saved signals from the server.
* @param {Object} comms - The communications instance.
* @param {Object} data - An object containing user data.
*/
fetchSavedSignals(comms, data) {
if (comms) {
try {
const requestData = {
request: 'signals',
user_name: data?.user_name
};
comms.sendToApp('request', requestData);
} catch (error) {
console.error("Error fetching saved signals:", error.message);
}
} else {
throw new Error('Communications instance not available.');
}
}
/**
* Adds a new signal to the local store.
* @param {Object} data - The signal data.
*/
addNewSignal(data) {
const signalData = data.signal || data;
console.log("Adding new signal:", signalData);
if (!signalData.name) {
console.error("Signal data missing 'name' field:", signalData);
return;
}
// Check for duplicates
const exists = this.signals.find(s => s.tbl_key === signalData.tbl_key || s.name === signalData.name);
if (!exists) {
this.signals.push(signalData);
}
}
/**
* Retrieves a signal by its tbl_key.
* @param {string} tbl_key - The tbl_key of the signal.
* @returns {Object|null} - The signal object or null.
*/
getSignalById(tbl_key) {
return this.signals.find(signal => signal.tbl_key === tbl_key) || null;
}
/**
* Retrieves a signal by its name.
* @param {string} name - The name of the signal.
* @returns {Object|null} - The signal object or null.
*/
getSignalByName(name) {
return this.signals.find(signal => signal.name === name) || null;
}
/**
* Updates signal data.
* @param {Object} data - The updated signal data.
*/
updateSignalData(data) {
const signalData = data.signal || data;
const signalKey = signalData.tbl_key || signalData.name;
if (!signalKey) return;
const index = this.signals.findIndex(
signal => signal.tbl_key === signalKey || signal.name === signalKey
);
if (index !== -1) {
this.signals[index] = { ...this.signals[index], ...signalData };
} else {
this.signals.push(signalData);
}
}
/**
* Removes a signal from the store.
* @param {string} identifier - The tbl_key or name of the signal.
*/
removeSignal(identifier) {
console.log(`Removing signal: ${identifier}`);
this.signals = this.signals.filter(
sig => sig.tbl_key !== identifier && sig.name !== identifier
);
}
/**
* Updates signal states from server updates.
* @param {Object} stateUpdates - Map of signal names to states.
*/
applyStateUpdates(stateUpdates) {
for (const name in stateUpdates) {
const signal = this.getSignalByName(name);
if (signal) {
signal.state = stateUpdates[name];
}
}
}
/**
* Returns all signals.
* @returns {Object[]} - The list of signals.
*/
getAllSignals() {
return this.signals;
}
/**
* Sets all signals (used when loading from server).
* @param {Object[]} signals - The list of signals.
*/
setSignals(signals) {
this.signals = Array.isArray(signals) ? signals : [];
}
}
/**
* Signals - Main coordinator class that manages SigUIManager, SigDataManager, and SocketIO communication
*/
class Signals {
constructor(ui) {
this.ui = ui;
this.comms = ui?.data?.comms;
this.indicatorData = ui?.data?.indicators;
this.data = ui?.data;
this.dataManager = new SigDataManager();
this.uiManager = new SigUIManager();
// Set up delete callback
this.uiManager.registerDeleteSignalCallback(this.deleteSignal.bind(this));
// Bind methods
this.submitSignal = this.submitSignal.bind(this);
this._initialized = false;
}
/**
* Initializes the Signals instance.
* @param {string} targetId - The ID of the signals container element.
* @param {string} formElId - The ID of the signal form element.
*/
initialize(targetId, formElId) {
try {
this.uiManager.initUI(targetId, formElId);
if (!this.comms) {
console.error("Communications instance not available.");
return;
}
// Register handlers with Comms
this.comms.on('signals', this.handleSignalsResponse.bind(this));
this.comms.on('signal_created', this.handleSignalCreated.bind(this));
this.comms.on('signal_updated', this.handleSignalUpdated.bind(this));
this.comms.on('signal_deleted', this.handleSignalDeleted.bind(this));
this.comms.on('signal_error', this.handleSignalError.bind(this));
this.comms.on('updates', this.handleUpdates.bind(this));
// Fetch saved signals
this.dataManager.fetchSavedSignals(this.comms, this.data);
this._initialized = true;
} catch (error) {
console.error("Error initializing Signals:", error);
}
}
/**
* Handle initial signals list response from server.
* @param {Array} data - List of signal objects.
*/
handleSignalsResponse(data) {
console.log("Received signals list:", data);
if (Array.isArray(data)) {
this.dataManager.setSignals(data);
this.uiManager.updateSignalsHtml(this.dataManager.getAllSignals());
}
}
/**
* Handle new signal created event.
* @param {Object} data - Server response with signal data.
*/
handleSignalCreated(data) {
console.log("Signal created:", data);
if (data.success) {
this.dataManager.addNewSignal(data);
this.uiManager.updateSignalsHtml(this.dataManager.getAllSignals());
} else {
alert(`Failed to create signal: ${data.message}`);
}
}
/**
* Handle signal updated event.
* @param {Object} data - Server response with updated signal data.
*/
handleSignalUpdated(data) {
console.log("Signal updated:", data);
if (data.success) {
this.dataManager.updateSignalData(data);
this.uiManager.updateSignalsHtml(this.dataManager.getAllSignals());
} else {
alert(`Failed to update signal: ${data.message}`);
}
}
/**
* Handle signal deleted event.
* @param {Object} data - Server response with deleted signal info.
*/
handleSignalDeleted(data) {
console.log("Signal deleted:", data);
const identifier = data.tbl_key || data.name;
if (identifier) {
this.dataManager.removeSignal(identifier);
this.uiManager.updateSignalsHtml(this.dataManager.getAllSignals());
}
}
/**
* Handle signal error.
* @param {Object} data - Error data.
*/
handleSignalError(data) {
console.error("Signal error:", data.message);
alert(`Signal error: ${data.message}`);
}
/**
* Handle updates (including signal state changes).
* @param {Object} data - Update data from server.
*/
handleUpdates(data) {
const { s_updates } = data;
if (s_updates) {
this.dataManager.applyStateUpdates(s_updates);
// Update UI for state changes
for (const name in s_updates) {
this.uiManager.updateSignalState(name, s_updates[name]);
}
}
}
// ================ Form Methods ================
/**
* Opens the signal creation form.
*/
open_signal_Form() {
this.uiManager.displayForm('new');
}
/**
* Closes the signal form.
*/
close_signal_Form() {
this.uiManager.hideForm();
}
/**
* Requests signals from server.
*/
request_signals() {
if (this.comms) {
this.comms.sendToApp('request', { request: 'signals', user_name: this.data?.user_name });
}
}
/**
* Deletes a signal by tbl_key or name.
* @param {string} identifier - The tbl_key or name of the signal.
*/
deleteSignal(identifier) {
if (!this.comms) {
console.error("Comms instance not available.");
return;
}
const signal = this.dataManager.getSignalById(identifier) ||
this.dataManager.getSignalByName(identifier);
const deleteData = signal
? { tbl_key: signal.tbl_key, name: signal.name }
: { name: identifier };
this.comms.sendToApp('delete_signal', deleteData);
}
/**
* Submits a new or edited signal.
* @param {string} action - 'new' or 'edit'.
*/
submitSignal(action) {
const formElement = this.uiManager.formElement;
if (!formElement) {
console.error("Form element not available.");
return;
}
const name = formElement.querySelector('#signal_name')?.value?.trim();
const source1 = formElement.querySelector('#sig_source')?.value;
const prop1 = formElement.querySelector('#sig_prop')?.value;
const source2 = formElement.querySelector('#sig2_source')?.value;
const prop2 = formElement.querySelector('#sig2_prop')?.value;
const operator = formElement.querySelector('input[name="Operator"]:checked')?.value;
const range = formElement.querySelector('#rangeVal')?.value;
const sigType = formElement.querySelector('#select_s_type')?.value;
const value = formElement.querySelector('#value')?.value;
const publicCheckbox = formElement.querySelector('#signal_public_checkbox');
const tblKey = formElement.querySelector('#signal_tbl_key')?.value;
if (!name) {
alert("Please provide a name for the signal.");
return;
}
if (!prop1) {
alert("Please select a property for the signal source.");
return;
}
// Build signal data
let actualSource2 = source2;
let actualProp2 = prop2;
if (sigType !== 'Comparison') {
actualSource2 = 'value';
actualProp2 = value;
}
const signalData = {
name,
source1,
prop1,
operator,
source2: actualSource2,
prop2: actualProp2,
state: false,
value1: null,
value2: null,
public: publicCheckbox?.checked ? 1 : 0,
user_name: this.data?.user_name
};
if (operator === '+/-') {
signalData.range = parseFloat(range) || 0;
}
if (action === 'edit' && tblKey) {
signalData.tbl_key = tblKey;
}
const messageType = action === 'new' ? 'new_signal' : 'edit_signal';
this.comms.sendToApp(messageType, signalData);
this.close_signal_Form();
}
/**
* Submits a new signal (legacy method name).
*/
submitNewSignal() {
this.submitSignal('new');
}
// ================ Helper Methods ================
/**
* Fills property dropdown based on indicator type.
* @param {string} target_id - The ID of the select element.
* @param {string} indctr - The indicator name.
*/
fill_prop(target_id, indctr) {
const target = document.getElementById(target_id);
const indicatorConfig = this.indicatorData ? this.indicatorData[indctr] : null;
if (!target) return;
// Clear existing options
while (target.options.length > 0) {
target.remove(0);
}
if (!indicatorConfig) {
console.warn(`Indicator "${indctr}" not found in indicator data`);
return;
}
// Get the indicator outputs based on type
const outputMap = {
'SMA': ['value'],
'EMA': ['value'],
'LREG': ['value'],
'RSI': ['value'],
'ATR': ['value'],
'Volume': ['value'],
'MACD': ['macd', 'signal', 'hist'],
'BOLBands': ['upper', 'middle', 'lower']
};
const indicatorType = indicatorConfig.type;
const outputs = outputMap[indicatorType] || ['value'];
for (const output of outputs) {
const opt = document.createElement("option");
opt.value = output;
opt.textContent = output;
target.appendChild(opt);
}
}
/**
* Switches between form panels.
* @param {string} p1 - Panel to hide.
* @param {string} p2 - Panel to show.
*/
switch_panel(p1, p2) {
const panel1 = document.getElementById(p1);
const panel2 = document.getElementById(p2);
if (panel1) panel1.style.display = 'none';
if (panel2) panel2.style.display = 'grid';
}
/**
* Conditionally hides an element.
* @param {*} firstValue - First value to compare.
* @param {*} scndValue - Second value to compare.
* @param {string} id - Element ID to show/hide.
*/
hideIfTrue(firstValue, scndValue, id) {
const el = document.getElementById(id);
if (el) {
el.style.display = firstValue === scndValue ? 'none' : 'block';
}
}
/**
* Gets the current value of an indicator property from the DOM.
* @param {string} source - The indicator source name.
* @param {string} prop - The property name.
* @returns {string} - The value.
*/
_getIndicatorValue(source, prop) {
let element = document.getElementById(source + '_' + prop)
|| document.getElementById(source + '_value');
if (!element) {
console.warn(`Could not find indicator value element for ${source}_${prop}`);
return '0';
}
let rawValue = element.value || element.textContent || '0';
// Parse multi-value format if needed
if (rawValue.includes(':') && rawValue.includes(',')) {
const parts = rawValue.split(',').map(p => p.trim());
for (const part of parts) {
const [key, val] = part.split(':').map(s => s.trim());
if (key === prop) {
return val;
}
}
}
return rawValue;
}
/**
* Handles panel 1 "Next" button click.
* @param {number} n - Panel number.
*/
ns_next(n) {
if (n === 1) {
const sigName = document.getElementById('signal_name')?.value;
const sigSource = document.getElementById('sig_source')?.value;
const sigProp = document.getElementById('sig_prop')?.value;
if (!sigName) {
alert('Please give the signal a name.');
return;
}
if (!sigProp) {
alert('Please select a property.');
return;
}
const display = document.getElementById('sig_display');
if (display) {
display.innerHTML = `${sigName}: {${sigSource}:${sigProp}}`;
}
// Get current indicator value
const indctrVal = this._getIndicatorValue(sigSource, sigProp);
const valueInput = document.getElementById('value');
if (valueInput) {
valueInput.value = indctrVal || '0';
}
this.switch_panel('panel_1', 'panel_2');
}
if (n === 2) {
const sigName = document.getElementById('signal_name')?.value;
const sigSource = document.getElementById('sig_source')?.value;
const sigProp = document.getElementById('sig_prop')?.value;
const sig2Source = document.getElementById('sig2_source')?.value;
const sig2Prop = document.getElementById('sig2_prop')?.value;
const operator = document.querySelector('input[name="Operator"]:checked')?.value;
const range = document.getElementById('rangeVal')?.value;
const sigType = document.getElementById('select_s_type')?.value;
const value = document.getElementById('value')?.value;
const sig1 = `${sigSource} : ${sigProp}`;
const sig2 = sigType === 'Comparison' ? `${sig2Source} : ${sig2Prop}` : value;
const operatorStr = operator === '+/-' ? `${operator} ${range}` : operator;
const sigDisplayStr = `(${sigName}) (${sig1}) (${operatorStr}) (${sig2})`;
const sig1_realtime = this._getIndicatorValue(sigSource, sigProp);
const sig2_realtime = sigType === 'Comparison'
? this._getIndicatorValue(sig2Source, sig2Prop)
: value;
const display2 = document.getElementById('sig_display2');
const realtime = document.getElementById('sig_realtime');
const evalEl = document.getElementById('sig_eval');
if (display2) display2.innerHTML = sigDisplayStr;
if (realtime) realtime.innerHTML = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`;
// Evaluate
let evalStr;
if (operator === '==') evalStr = parseFloat(sig1_realtime) === parseFloat(sig2_realtime);
if (operator === '>') evalStr = parseFloat(sig1_realtime) > parseFloat(sig2_realtime);
if (operator === '<') evalStr = parseFloat(sig1_realtime) < parseFloat(sig2_realtime);
if (operator === '+/-') evalStr = Math.abs(parseFloat(sig1_realtime) - parseFloat(sig2_realtime)) <= parseFloat(range);
if (evalEl) evalEl.innerHTML = evalStr ? 'true' : 'false';
this.switch_panel('panel_2', 'panel_3');
}
}
// Legacy methods for backwards compatibility
i_update(updates) {
for (const signal of this.dataManager.getAllSignals()) {
const s1 = signal.source1;
if (s1 in updates) {
const p1 = signal.prop1;
const value1 = updates[s1].data[0][p1];
signal.value1 = value1.toFixed(2);
}
if (signal.source2 !== 'value') {
const s2 = signal.source2;
if (s2 in updates) {
const p2 = signal.prop2;
const value2 = updates[s2].data[0][p2];
signal.value2 = value2.toFixed(2);
}
}
const val1El = document.getElementById(signal.name + '_value1');
const val2El = document.getElementById(signal.name + '_value2');
if (val1El) val1El.innerHTML = signal.value1;
if (val2El) val2El.innerHTML = signal.value2;
}
}
update_signal_states(s_updates) {
for (const name in s_updates) {
this.uiManager.updateSignalState(name, s_updates[name]);
}
}
set_data(signals) {
// Legacy method - now handled by handleSignalsResponse
if (Array.isArray(signals)) {
for (const sig of signals) {
const obj = typeof sig === 'string' ? JSON.parse(sig) : sig;
this.dataManager.addNewSignal(obj);
}
this.uiManager.updateSignalsHtml(this.dataManager.getAllSignals());
}
}
delete_signal(signal_name) {
// Legacy method - redirect to new method
this.deleteSignal(signal_name);
}
}