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

@@ -466,7 +466,7 @@ app.get('/api/node/status', async (req, res) => {
}); });
// Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=) // Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=)
app.get('/api/node/capabilities', async (req, res) => { app.get('/api/node/endpoints', async (req, res) => {
try { try {
const { ip } = req.query; const { ip } = req.query;
@@ -476,9 +476,9 @@ app.get('/api/node/capabilities', async (req, res) => {
const caps = await nodeClient.getCapabilities(); const caps = await nodeClient.getCapabilities();
return res.json(caps); return res.json(caps);
} catch (innerError) { } catch (innerError) {
console.error('Error fetching capabilities from specific node:', innerError); console.error('Error fetching endpoints from specific node:', innerError);
return res.status(500).json({ return res.status(500).json({
error: 'Failed to fetch capabilities from node', error: 'Failed to fetch endpoints from node',
message: innerError.message message: innerError.message
}); });
} }

View File

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

View File

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

View File

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

View File

@@ -2003,21 +2003,21 @@ p {
/* Capabilities Styles */ /* Capabilities Styles */
.capabilities-list { .endpoints-list {
margin-top: 0.5rem; margin-top: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
} }
/* Custom dropdown wrapper and arrow for capability selector */ /* Custom dropdown wrapper and arrow for endpoint selector */
.capability-selector { .endpoint-selector {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
#capability-select { #endpoint-select {
padding-right: 2rem; padding-right: 2rem;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23ecf0f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23ecf0f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>');
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -2025,21 +2025,21 @@ p {
background-size: 12px 12px; background-size: 12px 12px;
} }
.capability-item { .endpoint-item {
background: var(--bg-tertiary); background: var(--bg-tertiary);
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-radius: 8px; border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
} }
.capability-header { .endpoint-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.cap-method { .endpoint-method {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.5px; letter-spacing: 0.5px;
@@ -2050,14 +2050,14 @@ p {
color: var(--text-secondary); color: var(--text-secondary);
} }
.cap-uri { .endpoint-uri {
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 0.85rem; font-size: 0.85rem;
opacity: 1; opacity: 1;
flex: 1; flex: 1;
} }
.cap-call-btn { .endpoint-call-btn {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%); background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
border: 1px solid var(--border-secondary); border: 1px solid var(--border-secondary);
color: var(--text-secondary); color: var(--text-secondary);
@@ -2070,21 +2070,21 @@ p {
backdrop-filter: var(--backdrop-blur); backdrop-filter: var(--backdrop-blur);
} }
.cap-call-btn:hover { .endpoint-call-btn:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%); background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
border-color: rgba(255, 255, 255, 0.25); border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
} }
.capability-form { .endpoint-form {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.5rem 0.75rem; gap: 0.5rem 0.75rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.capability-param { .endpoint-param {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.25rem;
@@ -2124,13 +2124,13 @@ p {
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.15); box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.15);
} }
.capability-params.none { .endpoint-params.none {
opacity: 1; opacity: 1;
font-size: 0.85rem; font-size: 0.85rem;
padding: 0.25rem 0.25rem 0 0.25rem; padding: 0.25rem 0.25rem 0 0.25rem;
} }
.capability-result { .endpoint-result {
margin-top: 0.5rem; margin-top: 0.5rem;
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
@@ -2138,7 +2138,7 @@ p {
padding: 0.75rem; padding: 0.75rem;
} }
.cap-call-success { .endpoint-call-success {
color: var(--accent-success); color: var(--accent-success);
background: rgba(76, 175, 80, 0.1); background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2); border: 1px solid rgba(76, 175, 80, 0.2);
@@ -2146,7 +2146,7 @@ p {
border-radius: 6px; border-radius: 6px;
} }
.cap-call-error { .endpoint-call-error {
color: #f44336; color: #f44336;
background: rgba(244, 67, 54, 0.1); background: rgba(244, 67, 54, 0.1);
border: 1px solid rgba(244, 67, 54, 0.2); border: 1px solid rgba(244, 67, 54, 0.2);
@@ -2154,7 +2154,7 @@ p {
border-radius: 6px; border-radius: 6px;
} }
.cap-result-pre { .endpoint-result-pre {
margin-top: 0.5rem; margin-top: 0.5rem;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
@@ -2321,11 +2321,11 @@ p {
/* Responsive Capability Selector */ /* Responsive Capability Selector */
@media (max-width: 768px) { @media (max-width: 768px) {
.capability-selector { .endpoint-selector {
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
} }
#capability-select { #endpoint-select {
flex: 1 1 100%; flex: 1 1 100%;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -2334,23 +2334,23 @@ p {
} }
@media (max-width: 480px) { @media (max-width: 480px) {
#capability-select { #endpoint-select {
padding-right: 1.75rem; padding-right: 1.75rem;
background-position: right 0.5rem center; background-position: right 0.5rem center;
background-size: 10px 10px; background-size: 10px 10px;
} }
} }
/* Capability header mobile wrapping */ /* Endpoint header mobile wrapping */
@media (max-width: 768px) { @media (max-width: 768px) {
.capability-header { .endpoint-header {
flex-wrap: wrap; flex-wrap: wrap;
} }
.cap-uri { .endpoint-uri {
flex: 1 1 100%; flex: 1 1 100%;
min-width: 0; min-width: 0;
} }
.cap-call-btn { .endpoint-call-btn {
flex: 1 1 100%; flex: 1 1 100%;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
@@ -2359,7 +2359,7 @@ p {
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.cap-call-btn { .endpoint-call-btn {
padding: 0.35rem 0.75rem; padding: 0.35rem 0.75rem;
} }
} }

View File

@@ -85,11 +85,11 @@ class SporeApiClient {
} }
/** /**
* Get node capabilities * Get node endpoints
* @returns {Promise<Object>} Capabilities response * @returns {Promise<Object>} endpoints response
*/ */
async getCapabilities() { async getCapabilities() {
return this.request('GET', '/api/node/capabilities'); return this.request('GET', '/api/node/endpoints');
} }
/** /**