feat: auto-discovery

This commit is contained in:
2025-08-25 12:05:51 +02:00
parent f72e4ba220
commit 5977a37d6c
8 changed files with 552 additions and 3 deletions

View File

@@ -21,7 +21,18 @@
<div id="cluster-view" class="view-content active">
<div class="cluster-section">
<div class="cluster-header">
<div class="cluster-header-left"></div>
<div class="cluster-header-left">
<div class="primary-node-info">
<span class="primary-node-label">Primary Node:</span>
<span class="primary-node-ip" id="primary-node-ip">Discovering...</span>
<button class="primary-node-refresh" onclick="selectRandomPrimaryNode()" title="🎲 Select random primary node">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<path d="M1 4v6h6M23 20v-6h-6"/>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
</svg>
</button>
</div>
</div>
<button class="refresh-btn" onclick="refreshClusterMembers()">
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 4v6h6M23 20v-6h-6"/>

View File

@@ -23,6 +23,48 @@ class FrontendApiClient {
}
}
async getDiscoveryInfo() {
try {
const response = await fetch('/api/discovery/nodes', {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
}
async selectRandomPrimaryNode() {
try {
const response = await fetch('/api/discovery/random-primary', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
timestamp: new Date().toISOString()
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
}
async getNodeStatus(ip) {
try {
// Create a proxy endpoint that forwards the request to the specific node
@@ -66,6 +108,83 @@ class FrontendApiClient {
// Global client instance
const client = new FrontendApiClient();
// Function to update primary node display
async function updatePrimaryNodeDisplay() {
const primaryNodeElement = document.getElementById('primary-node-ip');
if (!primaryNodeElement) return;
try {
// Set discovering state
primaryNodeElement.textContent = 'Discovering...';
primaryNodeElement.className = 'primary-node-ip discovering';
const discoveryInfo = await client.getDiscoveryInfo();
if (discoveryInfo.primaryNode) {
const status = discoveryInfo.clientInitialized ? '✅' : '⚠️';
const nodeCount = discoveryInfo.totalNodes > 1 ? ` (${discoveryInfo.totalNodes} nodes)` : '';
primaryNodeElement.textContent = `${status} ${discoveryInfo.primaryNode}${nodeCount}`;
primaryNodeElement.className = 'primary-node-ip';
} else if (discoveryInfo.totalNodes > 0) {
// If we have nodes but no primary, show the first one
const firstNode = discoveryInfo.nodes[0];
primaryNodeElement.textContent = `⚠️ ${firstNode.ip} (No Primary)`;
primaryNodeElement.className = 'primary-node-ip error';
} else {
primaryNodeElement.textContent = '🔍 No Nodes Found';
primaryNodeElement.className = 'primary-node-ip error';
}
} catch (error) {
console.error('Failed to fetch discovery info:', error);
primaryNodeElement.textContent = '❌ Discovery Failed';
primaryNodeElement.className = 'primary-node-ip error';
}
}
// Function to randomly select a new primary node
async function selectRandomPrimaryNode() {
const primaryNodeElement = document.getElementById('primary-node-ip');
if (!primaryNodeElement) return;
try {
// Store current primary node for reference
const currentText = primaryNodeElement.textContent;
const currentPrimary = currentText.includes('✅') || currentText.includes('⚠️') ?
currentText.split(' ')[1] : 'unknown';
// Set selecting state
primaryNodeElement.textContent = '🎲 Selecting...';
primaryNodeElement.className = 'primary-node-ip selecting';
// Call the random selection API
const result = await client.selectRandomPrimaryNode();
if (result.success) {
// Show success message briefly
primaryNodeElement.textContent = `🎯 ${result.primaryNode}`;
primaryNodeElement.className = 'primary-node-ip';
// Update the display after a short delay
setTimeout(() => {
updatePrimaryNodeDisplay();
}, 1500);
console.log(`Randomly selected new primary node: ${result.primaryNode}`);
} else {
throw new Error(result.message || 'Random selection failed');
}
} catch (error) {
console.error('Failed to select random primary node:', error);
primaryNodeElement.textContent = '❌ Selection Failed';
primaryNodeElement.className = 'primary-node-ip error';
// Revert to normal display after error
setTimeout(() => {
updatePrimaryNodeDisplay();
}, 2000);
}
}
// Function to refresh cluster members
async function refreshClusterMembers() {
const container = document.getElementById('cluster-members-container');
@@ -97,6 +216,9 @@ async function refreshClusterMembers() {
const response = await client.getClusterMembers();
console.log(response);
displayClusterMembers(response.members, expandedCards);
// Update primary node display after cluster refresh
await updatePrimaryNodeDisplay();
} catch (error) {
console.error('Failed to fetch cluster members:', error);
container.innerHTML = `
@@ -105,6 +227,9 @@ async function refreshClusterMembers() {
${error.message}
</div>
`;
// Still try to update primary node display even if cluster fails
await updatePrimaryNodeDisplay();
}
}
@@ -533,8 +658,12 @@ function displayClusterMembers(members, expandedCards = new Map()) {
// Load cluster members when page loads
document.addEventListener('DOMContentLoaded', function() {
refreshClusterMembers();
updatePrimaryNodeDisplay(); // Also update primary node display
setupNavigation();
setupFirmwareView();
// Set up periodic primary node updates (every 10 seconds)
setInterval(updatePrimaryNodeDisplay, 10000);
});
// Auto-refresh every 30 seconds

View File

@@ -48,6 +48,88 @@ p {
/* Placeholder for future content if needed */
}
.primary-node-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.primary-node-label {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
}
.primary-node-ip {
font-size: 0.9rem;
color: #4ade80;
font-weight: 600;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
padding: 0.25rem 0.5rem;
background: rgba(74, 222, 128, 0.1);
border-radius: 4px;
border: 1px solid rgba(74, 222, 128, 0.2);
}
.primary-node-ip.discovering {
color: #fbbf24;
background: rgba(251, 191, 36, 0.1);
border-color: rgba(251, 191, 36, 0.2);
}
.primary-node-ip.error {
color: #f87171;
background: rgba(248, 113, 113, 0.1);
border-color: rgba(248, 113, 113, 0.2);
margin: 0;
padding: 0.25rem 0.5rem;
}
.primary-node-ip.selecting {
color: #8b5cf6;
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.2);
animation: pulse 1.5s ease-in-out infinite alternate;
}
@keyframes pulse {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0.7;
transform: scale(1.05);
}
}
.primary-node-refresh {
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.primary-node-refresh:hover {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.1);
}
.primary-node-refresh:active {
transform: scale(0.95);
}
.firmware-header {
display: flex;
justify-content: space-between;