refactor: remove capabilities in favor of endpoints

This commit is contained in:
2025-09-13 19:15:38 +02:00
parent 3055c2cb0e
commit d0557a56a2
6 changed files with 98 additions and 104 deletions

View File

@@ -80,11 +80,11 @@ class ApiClient {
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
}
async getCapabilities(ip) {
return this.request('/api/node/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
async getEndpoints(ip) {
return this.request('/api/node/endpoints', { method: 'GET', query: ip ? { ip } : undefined });
}
async callCapability({ ip, method, uri, params }) {
async callEndpoint({ ip, method, uri, params }) {
return this.request('/api/proxy-call', {
method: 'POST',
body: { ip, method, uri, params }

View File

@@ -10,13 +10,13 @@ class NodeDetailsComponent extends Component {
this.subscribeToProperty('isLoading', this.handleLoadingUpdate.bind(this));
this.subscribeToProperty('error', this.handleErrorUpdate.bind(this));
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
this.subscribeToProperty('capabilities', this.handleCapabilitiesUpdate.bind(this));
this.subscribeToProperty('endpoints', this.handleEndpointsUpdate.bind(this));
}
// Handle node status update with state preservation
handleNodeStatusUpdate(newStatus, previousStatus) {
if (newStatus && !this.viewModel.get('isLoading')) {
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('capabilities'));
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints'));
}
}
@@ -24,7 +24,7 @@ class NodeDetailsComponent extends Component {
handleTasksUpdate(newTasks, previousTasks) {
const nodeStatus = this.viewModel.get('nodeStatus');
if (nodeStatus && !this.viewModel.get('isLoading')) {
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('capabilities'));
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('endpoints'));
}
}
@@ -48,12 +48,12 @@ class NodeDetailsComponent extends Component {
this.updateActiveTab(newTab, previousTab);
}
// Handle capabilities update with state preservation
handleCapabilitiesUpdate(newCapabilities, previousCapabilities) {
// Handle endpoints update with state preservation
handleEndpointsUpdate(newEndpoints, previousEndpoints) {
const nodeStatus = this.viewModel.get('nodeStatus');
const tasks = this.viewModel.get('tasks');
if (nodeStatus && !this.viewModel.get('isLoading')) {
this.renderNodeDetails(nodeStatus, tasks, newCapabilities);
this.renderNodeDetails(nodeStatus, tasks, newEndpoints);
}
}
@@ -62,7 +62,7 @@ class NodeDetailsComponent extends Component {
const tasks = this.viewModel.get('tasks');
const isLoading = this.viewModel.get('isLoading');
const error = this.viewModel.get('error');
const capabilities = this.viewModel.get('capabilities');
const endpoints = this.viewModel.get('endpoints');
if (isLoading) {
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
@@ -79,10 +79,10 @@ class NodeDetailsComponent extends Component {
return;
}
this.renderNodeDetails(nodeStatus, tasks, capabilities);
this.renderNodeDetails(nodeStatus, tasks, endpoints);
}
renderNodeDetails(nodeStatus, tasks, capabilities) {
renderNodeDetails(nodeStatus, tasks, endpoints) {
// Use persisted active tab from the view model, default to 'status'
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
@@ -92,7 +92,6 @@ class NodeDetailsComponent extends Component {
<div class="tabs-header">
<button class="tab-button ${activeTab === 'status' ? 'active' : ''}" data-tab="status">Status</button>
<button class="tab-button ${activeTab === 'endpoints' ? 'active' : ''}" data-tab="endpoints">Endpoints</button>
<button class="tab-button ${activeTab === 'capabilities' ? 'active' : ''}" data-tab="capabilities">Capabilities</button>
<button class="tab-button ${activeTab === 'tasks' ? 'active' : ''}" data-tab="tasks">Tasks</button>
<button class="tab-button ${activeTab === 'firmware' ? 'active' : ''}" data-tab="firmware">Firmware</button>
</div>
@@ -121,14 +120,9 @@ class NodeDetailsComponent extends Component {
</div>
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
${nodeStatus.api ? nodeStatus.api.map(endpoint =>
`<div class="endpoint-item">${endpoint.method === 1 ? 'GET' : 'POST'} ${endpoint.uri}</div>`
).join('') : '<div class="endpoint-item">No API endpoints available</div>'}
${this.renderEndpointsTab(endpoints)}
</div>
<div class="tab-content ${activeTab === 'capabilities' ? 'active' : ''}" id="capabilities-tab">
${this.renderCapabilitiesTab(capabilities)}
</div>
<div class="tab-content ${activeTab === 'tasks' ? 'active' : ''}" id="tasks-tab">
${this.renderTasksTab(tasks)}
@@ -150,18 +144,18 @@ class NodeDetailsComponent extends Component {
this.setupFirmwareUpload();
}
renderCapabilitiesTab(capabilities) {
if (!capabilities || !Array.isArray(capabilities.endpoints) || capabilities.endpoints.length === 0) {
renderEndpointsTab(endpoints) {
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
return `
<div class="no-capabilities">
<div>🧩 No capabilities reported</div>
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">This node did not return any capabilities</div>
<div class="no-endpoints">
<div>🧩 No endpoints reported</div>
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">This node did not return any endpoints</div>
</div>
`;
}
// Sort endpoints by URI (name), then by method for stable ordering
const endpoints = [...capabilities.endpoints].sort((a, b) => {
const endpointsList = [...endpoints.endpoints].sort((a, b) => {
const aUri = String(a.uri || '').toLowerCase();
const bUri = String(b.uri || '').toLowerCase();
if (aUri < bUri) return -1;
@@ -171,22 +165,22 @@ class NodeDetailsComponent extends Component {
return aMethod.localeCompare(bMethod);
});
const total = endpoints.length;
const total = endpointsList.length;
// Preserve selection based on a stable key of method+uri if available
const selectedKey = String(this.getUIState('capSelectedKey') || '');
let selectedIndex = endpoints.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey);
const selectedKey = String(this.getUIState('endpointSelectedKey') || '');
let selectedIndex = endpointsList.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey);
if (selectedIndex === -1) {
selectedIndex = Number(this.getUIState('capSelectedIndex'));
selectedIndex = Number(this.getUIState('endpointSelectedIndex'));
if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= total) {
selectedIndex = 0;
}
}
// Compute padding for aligned display in dropdown
const maxMethodLen = endpoints.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0);
const maxMethodLen = endpointsList.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0);
const selectorOptions = endpoints.map((ep, idx) => {
const selectorOptions = endpointsList.map((ep, idx) => {
const method = String(ep.method || '');
const uri = String(ep.uri || '');
const padCount = Math.max(1, (maxMethodLen - method.length) + 2);
@@ -194,12 +188,12 @@ class NodeDetailsComponent extends Component {
return `<option value="${idx}" data-method="${method}" data-uri="${uri}" ${idx === selectedIndex ? 'selected' : ''}>${method}${spacer}${uri}</option>`;
}).join('');
const items = endpoints.map((ep, idx) => {
const formId = `cap-form-${idx}`;
const resultId = `cap-result-${idx}`;
const items = endpointsList.map((ep, idx) => {
const formId = `endpoint-form-${idx}`;
const resultId = `endpoint-result-${idx}`;
const params = Array.isArray(ep.params) && ep.params.length > 0
? `<div class="capability-params">${ep.params.map((p, pidx) => `
<label class="capability-param" for="${formId}-field-${pidx}">
? `<div class="endpoint-params">${ep.params.map((p, pidx) => `
<label class="endpoint-param" for="${formId}-field-${pidx}">
<span class="param-name">${p.name}${p.required ? ' *' : ''}</span>
${ (Array.isArray(p.values) && p.values.length > 1)
? `<select id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input">${p.values.map(v => `<option value="${v}">${v}</option>`).join('')}</select>`
@@ -207,54 +201,54 @@ class NodeDetailsComponent extends Component {
}
</label>
`).join('')}</div>`
: '<div class="capability-params none">No parameters</div>';
: '<div class="endpoint-params none">No parameters</div>';
return `
<div class="capability-item" data-cap-index="${idx}" style="display:${idx === selectedIndex ? '' : 'none'};">
<div class="capability-header">
<span class="cap-method">${ep.method}</span>
<span class="cap-uri">${ep.uri}</span>
<button class="cap-call-btn" data-action="call-capability" data-method="${ep.method}" data-uri="${ep.uri}" data-form-id="${formId}" data-result-id="${resultId}">Call</button>
<div class="endpoint-item" data-endpoint-index="${idx}" style="display:${idx === selectedIndex ? '' : 'none'};">
<div class="endpoint-header">
<span class="endpoint-method">${ep.method}</span>
<span class="endpoint-uri">${ep.uri}</span>
<button class="endpoint-call-btn" data-action="call-endpoint" data-method="${ep.method}" data-uri="${ep.uri}" data-form-id="${formId}" data-result-id="${resultId}">Call</button>
</div>
<form id="${formId}" class="capability-form" onsubmit="return false;">
<form id="${formId}" class="endpoint-form" onsubmit="return false;">
${params}
</form>
<div id="${resultId}" class="capability-result" style="display:none;"></div>
<div id="${resultId}" class="endpoint-result" style="display:none;"></div>
</div>
`;
}).join('');
// Attach events after render in setupCapabilitiesEvents()
setTimeout(() => this.setupCapabilitiesEvents(), 0);
// Attach events after render in setupEndpointsEvents()
setTimeout(() => this.setupEndpointsEvents(), 0);
return `
<div class="capability-selector">
<label class="param-name" for="capability-select">Capability</label>
<select id="capability-select" class="param-input" style="font-family: monospace;">${selectorOptions}</select>
<div class="endpoint-selector">
<label class="param-name" for="endpoint-select">Endpoint</label>
<select id="endpoint-select" class="param-input" style="font-family: monospace;">${selectorOptions}</select>
</div>
<div class="capabilities-list">${items}</div>
<div class="endpoints-list">${items}</div>
`;
}
setupCapabilitiesEvents() {
const selector = this.findElement('#capability-select');
setupEndpointsEvents() {
const selector = this.findElement('#endpoint-select');
if (selector) {
this.addEventListener(selector, 'change', (e) => {
const selected = Number(e.target.value);
const items = Array.from(this.findAllElements('.capability-item'));
const items = Array.from(this.findAllElements('.endpoint-item'));
items.forEach((el, idx) => {
el.style.display = (idx === selected) ? '' : 'none';
});
this.setUIState('capSelectedIndex', selected);
this.setUIState('endpointSelectedIndex', selected);
const opt = e.target.selectedOptions && e.target.selectedOptions[0];
if (opt) {
const method = opt.dataset.method || '';
const uri = opt.dataset.uri || '';
this.setUIState('capSelectedKey', `${method} ${uri}`);
this.setUIState('endpointSelectedKey', `${method} ${uri}`);
}
});
}
const buttons = this.findAllElements('.cap-call-btn');
const buttons = this.findAllElements('.endpoint-call-btn');
buttons.forEach(btn => {
this.addEventListener(btn, 'click', async (e) => {
e.stopPropagation();
@@ -281,7 +275,7 @@ class NodeDetailsComponent extends Component {
if (missing.length > 0) {
resultEl.style.display = 'block';
resultEl.innerHTML = `
<div class="cap-call-error">
<div class="endpoint-call-error">
<div>❌ Missing required fields: ${missing.map(m => this.escapeHtml(m.name)).join(', ')}</div>
</div>
`;
@@ -293,17 +287,17 @@ class NodeDetailsComponent extends Component {
resultEl.innerHTML = '<div class="loading">Calling endpoint...</div>';
try {
const response = await this.viewModel.callCapability(method, uri, params);
const response = await this.viewModel.callEndpoint(method, uri, params);
const pretty = typeof response?.data === 'object' ? JSON.stringify(response.data, null, 2) : String(response?.data ?? '');
resultEl.innerHTML = `
<div class="cap-call-success">
<div class="endpoint-call-success">
<div>✅ Success</div>
<pre class="cap-result-pre">${this.escapeHtml(pretty)}</pre>
<pre class="endpoint-result-pre">${this.escapeHtml(pretty)}</pre>
</div>
`;
} catch (err) {
resultEl.innerHTML = `
<div class="cap-call-error">
<div class="endpoint-call-error">
<div>❌ Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
</div>
`;

View File

@@ -219,7 +219,7 @@ class NodeDetailsViewModel extends ViewModel {
error: null,
activeTab: 'status',
nodeIp: null,
capabilities: null,
endpoints: null,
tasksSummary: null
});
}
@@ -247,8 +247,8 @@ class NodeDetailsViewModel extends ViewModel {
// Load tasks data
await this.loadTasksData();
// Load capabilities data
await this.loadCapabilitiesData();
// Load endpoints data
await this.loadEndpointsData();
} catch (error) {
console.error('Failed to load node details:', error);
@@ -272,22 +272,22 @@ class NodeDetailsViewModel extends ViewModel {
}
}
// Load capabilities data with state preservation
async loadCapabilitiesData() {
// Load endpoints data with state preservation
async loadEndpointsData() {
try {
const ip = this.get('nodeIp');
const response = await window.apiClient.getCapabilities(ip);
this.set('capabilities', response || null);
const response = await window.apiClient.getEndpoints(ip);
this.set('endpoints', response || null);
} catch (error) {
console.error('Failed to load capabilities:', error);
this.set('capabilities', null);
console.error('Failed to load endpoints:', error);
this.set('endpoints', null);
}
}
// Invoke a capability against this node
async callCapability(method, uri, params) {
// Invoke an endpoint against this node
async callEndpoint(method, uri, params) {
const ip = this.get('nodeIp');
return window.apiClient.callCapability({ ip, method, uri, params });
return window.apiClient.callEndpoint({ ip, method, uri, params });
}
// Set active tab with state persistence