feature/capabilities (#2)

Reviewed-on: #2
This commit is contained in:
2025-08-28 11:17:37 +02:00
parent 6c58e479af
commit bb46e5d412
6 changed files with 504 additions and 35 deletions

132
index.js
View File

@@ -394,7 +394,45 @@ app.get('/api/tasks/status', async (req, res) => {
// API endpoint to get system status
app.get('/api/node/status', async (req, res) => {
try {
if (!sporeClient) {
return res.status(503).json({
error: 'Service unavailable',
message: 'No SPORE nodes discovered yet. Waiting for CLUSTER_DISCOVERY messages...',
discoveredNodes: Array.from(discoveredNodes.keys())
});
}
const systemStatus = await sporeClient.getSystemStatus();
res.json(systemStatus);
} catch (error) {
console.error('Error fetching system status:', error);
res.status(500).json({
error: 'Failed to fetch system status',
message: error.message
});
}
});
// Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=)
app.get('/api/capabilities', async (req, res) => {
try {
const { ip } = req.query;
if (ip) {
try {
const nodeClient = new SporeApiClient(`http://${ip}`);
const caps = await nodeClient.getCapabilities();
return res.json(caps);
} catch (innerError) {
console.error('Error fetching capabilities from specific node:', innerError);
return res.status(500).json({
error: 'Failed to fetch capabilities from node',
message: innerError.message
});
}
}
if (!sporeClient) {
return res.status(503).json({
error: 'Service unavailable',
@@ -402,14 +440,94 @@ app.get('/api/node/status', async (req, res) => {
discoveredNodes: Array.from(discoveredNodes.keys())
});
}
const systemStatus = await sporeClient.getSystemStatus();
res.json(systemStatus);
const caps = await sporeClient.getCapabilities();
return res.json(caps);
} catch (error) {
console.error('Error fetching system status:', error);
res.status(500).json({
error: 'Failed to fetch system status',
message: error.message
console.error('Error fetching capabilities:', error);
return res.status(500).json({
error: 'Failed to fetch capabilities',
message: error.message
});
}
});
// Generic proxy to call a node capability directly
app.post('/api/proxy-call', async (req, res) => {
try {
const { ip, method, uri, params } = req.body || {};
if (!ip || !method || !uri) {
return res.status(400).json({
error: 'Missing required fields',
message: 'Required: ip, method, uri'
});
}
// Build target URL
let targetPath = uri;
let queryParams = new URLSearchParams();
let bodyParams = new URLSearchParams();
if (Array.isArray(params)) {
for (const p of params) {
const name = p?.name;
const value = p?.value ?? '';
const location = (p?.location || 'body').toLowerCase();
if (!name) continue;
if (location === 'query') {
queryParams.append(name, String(value));
} else if (location === 'path') {
// Replace {name} or :name in path
targetPath = targetPath.replace(new RegExp(`[{:]${name}[}]?`, 'g'), encodeURIComponent(String(value)));
} else {
// Default to body
bodyParams.append(name, String(value));
}
}
}
const queryString = queryParams.toString();
const fullUrl = `http://${ip}${targetPath}${queryString ? `?${queryString}` : ''}`;
// Prepare fetch options
const upperMethod = String(method).toUpperCase();
const fetchOptions = { method: upperMethod, headers: {} };
if (upperMethod !== 'GET') {
// Default to form-encoded body for generic proxy
fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
fetchOptions.body = bodyParams.toString();
}
// Execute request
const response = await fetch(fullUrl, fetchOptions);
const respContentType = response.headers.get('content-type') || '';
let data;
if (respContentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
if (!response.ok) {
return res.status(response.status).json({
error: 'Upstream request failed',
status: response.status,
statusText: response.statusText,
data
});
}
return res.json({ success: true, data });
} catch (error) {
console.error('Error in /api/proxy-call:', error);
return res.status(500).json({
error: 'Proxy call failed',
message: error.message
});
}
});