feat: replace all emojis with SVG icons

This commit is contained in:
2025-10-14 10:17:38 +02:00
parent 55bc38577c
commit fa6d72ea62
10 changed files with 240 additions and 73 deletions

View File

@@ -18,10 +18,37 @@
</svg>
</button>
<div class="nav-left">
<button class="nav-tab active" data-view="cluster">🌐 Cluster</button>
<button class="nav-tab" data-view="topology">🔗 Topology</button>
<button class="nav-tab" data-view="monitoring">📡 Monitoring</button>
<button class="nav-tab" data-view="firmware">📦 Firmware</button>
<button class="nav-tab active" data-view="cluster">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<circle cx="12" cy="12" r="9"/>
<circle cx="8" cy="10" r="1.5"/>
<circle cx="16" cy="8" r="1.5"/>
<circle cx="14" cy="15" r="1.5"/>
<path d="M9 11l3 3M9 11l6-3"/>
</svg>
Cluster
</button>
<button class="nav-tab" data-view="topology">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<path d="M6 3v6M6 15v6M18 3v6M18 15v6"/>
<rect x="3" y="9" width="6" height="6" rx="2"/>
<rect x="15" y="9" width="6" height="6" rx="2"/>
</svg>
Topology
</button>
<button class="nav-tab" data-view="monitoring">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>
</svg>
Monitoring
</button>
<button class="nav-tab" data-view="firmware">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
<path d="M12 8v8"/>
</svg>
Firmware
</button>
</div>
<div class="nav-right">
<div class="theme-switcher">
@@ -32,7 +59,7 @@
</svg>
</button>
</div>
<div class="cluster-status">🚀 Cluster Online</div>
<div class="cluster-status">Cluster Online</div>
</div>
</div>
@@ -44,7 +71,7 @@
<span class="primary-node-label">Primary Node:</span>
<span class="primary-node-ip" id="primary-node-ip">Discovering...</span>
<button class="primary-node-refresh" id="select-random-primary-btn"
title="🎲 Select random primary node">
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" />
@@ -85,7 +112,13 @@
<div class="firmware-overview">
<div class="firmware-actions">
<div class="action-group">
<h3>🚀 Firmware Update</h3>
<h3>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
<path d="M12 8v8"/>
</svg>
Firmware Update
</h3>
<div class="firmware-upload-compact">
<div class="compact-upload-row">
<div class="file-upload-area">
@@ -120,13 +153,24 @@
style="display: none;">
<button class="upload-btn-compact"
onclick="document.getElementById('global-firmware-file').click()">
📁 Choose File
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:6px; vertical-align: -2px;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<path d="M14 2v6h6"/>
</svg>
Choose File
</button>
<span class="file-info" id="file-info">No file selected</span>
</div>
</div>
<button class="deploy-btn" id="deploy-btn" disabled>🚀 Deploy</button>
<button class="deploy-btn" id="deploy-btn" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
<path d="M12 16V4"/>
<path d="M8 8l4-4 4 4"/>
<path d="M20 16v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/>
</svg>
Deploy
</button>
</div>
</div>
</div>
@@ -143,7 +187,12 @@
<div id="monitoring-view" class="view-content">
<div class="monitoring-view-section">
<div class="monitoring-header">
<h2>📡 Monitoring</h2>
<h2>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18" style="margin-right:8px; vertical-align: -2px;">
<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>
</svg>
Monitoring
</h2>
<button class="refresh-btn" id="refresh-monitoring-btn">
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 4v6h6M23 20v-6h-6" />
@@ -172,6 +221,7 @@
<script src="./vendor/d3.v7.min.js"></script>
<script src="./scripts/constants.js"></script>
<script src="./scripts/icons.js"></script>
<script src="./scripts/framework.js"></script>
<script src="./scripts/api-client.js"></script>
<script src="./scripts/view-models.js"></script>

View File

@@ -302,7 +302,7 @@ class ClusterMembersComponent extends Component {
const statusElement = card.querySelector('.member-status');
if (statusElement) {
const statusClass = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'status-online' : 'status-offline';
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? '🟢' : '🔴';
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? window.icon('dotGreen', { width: 12, height: 12 }) : window.icon('dotRed', { width: 12, height: 12 });
statusElement.className = `member-status ${statusClass}`;
statusElement.innerHTML = `${statusIcon}`;
@@ -392,7 +392,7 @@ class ClusterMembersComponent extends Component {
logger.debug('ClusterMembersComponent: showEmptyState() called');
this.renderEmpty(`
<div class="empty-state">
<div class="empty-state-icon">🌐</div>
<div class="empty-state-icon">${window.icon('cluster', { width: 24, height: 24 })}</div>
<div>No cluster members found</div>
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">
The cluster might be empty or not yet discovered
@@ -406,7 +406,7 @@ class ClusterMembersComponent extends Component {
const membersHTML = members.map(member => {
const statusClass = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'status-online' : 'status-offline';
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? '🟢' : '🔴';
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? window.icon('dotGreen', { width: 12, height: 12 }) : window.icon('dotRed', { width: 12, height: 12 });
logger.debug('ClusterMembersComponent: Rendering member:', member);

View File

@@ -20,19 +20,19 @@ class ClusterStatusComponent extends Component {
if (error) {
statusText = 'Cluster Error';
statusIcon = '❌';
statusIcon = window.icon('error', { width: 12, height: 12 });
statusClass = 'cluster-status-error';
} else if (totalNodes === 0) {
statusText = 'Cluster Offline';
statusIcon = '🔴';
statusIcon = window.icon('dotRed', { width: 12, height: 12 });
statusClass = 'cluster-status-offline';
} else if (clientInitialized) {
statusText = 'Cluster Online';
statusIcon = '🟢';
statusIcon = window.icon('dotGreen', { width: 12, height: 12 });
statusClass = 'cluster-status-online';
} else {
statusText = 'Cluster Connecting';
statusIcon = '🟡';
statusIcon = window.icon('dotYellow', { width: 12, height: 12 });
statusClass = 'cluster-status-connecting';
}

View File

@@ -357,7 +357,14 @@ class FirmwareComponent extends Component {
const progressHTML = `
<div class="firmware-upload-progress" id="firmware-upload-progress">
<div class="progress-header">
<h3>📤 Firmware Upload Progress</h3>
<h3>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
<path d="M12 16V4"/>
<path d="M8 8l4-4 4 4"/>
<path d="M20 16v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/>
</svg>
Firmware Upload Progress
</h3>
<div class="progress-info">
<span>File: ${file.name}</span>
<span>Size: ${(file.size / 1024).toFixed(1)}KB</span>
@@ -470,20 +477,20 @@ class FirmwareComponent extends Component {
if (totalCount === 1) {
// Single node upload
if (successCount === 1) {
progressHeader.textContent = `📤 Firmware Upload Complete`;
progressSummary.innerHTML = `<span> Upload to ${results[0].hostname || results[0].nodeIp} completed successfully at ${new Date().toLocaleTimeString()}</span>`;
progressHeader.textContent = `Firmware Upload Complete`;
progressSummary.innerHTML = `<span>${window.icon('success', { width: 14, height: 14 })} Upload to ${results[0].hostname || results[0].nodeIp} completed successfully at ${new Date().toLocaleTimeString()}</span>`;
} else {
progressHeader.textContent = `📤 Firmware Upload Failed`;
progressSummary.innerHTML = `<span> Upload to ${results[0].hostname || results[0].nodeIp} failed at ${new Date().toLocaleTimeString()}</span>`;
progressHeader.textContent = `Firmware Upload Failed`;
progressSummary.innerHTML = `<span>${window.icon('error', { width: 14, height: 14 })} Upload to ${results[0].hostname || results[0].nodeIp} failed at ${new Date().toLocaleTimeString()}</span>`;
}
} else if (successCount === totalCount) {
// Multi-node upload - all successful
progressHeader.textContent = `📤 Firmware Upload Complete (${successCount}/${totalCount} Successful)`;
progressSummary.innerHTML = `<span> All uploads completed successfully at ${new Date().toLocaleTimeString()}</span>`;
progressHeader.textContent = `Firmware Upload Complete (${successCount}/${totalCount} Successful)`;
progressSummary.innerHTML = `<span>${window.icon('success', { width: 14, height: 14 })} All uploads completed successfully at ${new Date().toLocaleTimeString()}</span>`;
} else {
// Multi-node upload - some failed
progressHeader.textContent = `📤 Firmware Upload Results (${successCount}/${totalCount} Successful)`;
progressSummary.innerHTML = `<span>⚠️ Upload completed with ${totalCount - successCount} failure(s) at ${new Date().toLocaleTimeString()}</span>`;
progressHeader.textContent = `Firmware Upload Results (${successCount}/${totalCount} Successful)`;
progressSummary.innerHTML = `<span>${window.icon('warning', { width: 14, height: 14 })} Upload completed with ${totalCount - successCount} failure(s) at ${new Date().toLocaleTimeString()}</span>`;
}
}
}
@@ -682,7 +689,7 @@ class FirmwareComponent extends Component {
}
const html = `
<div class="affected-nodes">
<div class="progress-header"><h3>🎯 Affected Nodes (${nodes.length})</h3></div>
<div class="progress-header"><h3>Affected Nodes (${nodes.length})</h3></div>
<div class="progress-list">
${nodes.map(n => `
<div class="progress-item" data-node-ip="${n.ip}">

View File

@@ -236,7 +236,7 @@ class MonitoringViewComponent extends Component {
if (error) {
container.innerHTML = `
<div class="error">
<div>❌ Error: ${error}</div>
<div>${window.icon('error', { width: 14, height: 14, class: 'icon' })} Error: ${this.escapeHtml(String(error))}</div>
</div>
`;
return;
@@ -259,7 +259,7 @@ class MonitoringViewComponent extends Component {
<div class="summary-stats">
<div class="stat-card">
<div class="stat-icon">🖥️</div>
<div class="stat-icon">${window.icon('computer', { width: 18, height: 18 })}</div>
<div class="stat-content">
<div class="stat-label">Total Nodes</div>
<div class="stat-value">${clusterSummary.totalNodes}</div>
@@ -267,7 +267,7 @@ class MonitoringViewComponent extends Component {
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-icon">${window.icon('cpu', { width: 18, height: 18 })}</div>
<div class="stat-content">
<div class="stat-label">CPU</div>
<div class="stat-value">${Math.round(clusterSummary.totalCpu - clusterSummary.availableCpu)}MHz / ${Math.round(clusterSummary.totalCpu)}MHz</div>
@@ -281,7 +281,7 @@ class MonitoringViewComponent extends Component {
</div>
<div class="stat-card">
<div class="stat-icon">🧠</div>
<div class="stat-icon">${window.icon('memory', { width: 18, height: 18 })}</div>
<div class="stat-content">
<div class="stat-label">Memory</div>
<div class="stat-value">${this.viewModel.formatResourceValue(clusterSummary.totalMemory - clusterSummary.availableMemory, 'memory')} / ${this.viewModel.formatResourceValue(clusterSummary.totalMemory, 'memory')}</div>
@@ -295,7 +295,7 @@ class MonitoringViewComponent extends Component {
</div>
<div class="stat-card">
<div class="stat-icon">💾</div>
<div class="stat-icon">${window.icon('storage', { width: 18, height: 18 })}</div>
<div class="stat-content">
<div class="stat-label">Storage</div>
<div class="stat-value">${this.viewModel.formatResourceValue(clusterSummary.totalStorage - clusterSummary.availableStorage, 'storage')} / ${this.viewModel.formatResourceValue(clusterSummary.totalStorage, 'storage')}</div>
@@ -328,7 +328,7 @@ class MonitoringViewComponent extends Component {
if (error) {
container.innerHTML = `
<div class="error">
<div>❌ Error: ${error}</div>
<div>${window.icon('error', { width: 14, height: 14, class: 'icon' })} Error: ${this.escapeHtml(String(error))}</div>
</div>
`;
return;
@@ -357,7 +357,7 @@ class MonitoringViewComponent extends Component {
const nodeCount = nodeResources.size;
container.innerHTML = `
<div class="nodes-monitoring-content">
<h3>🖥️ Node Resource Details</h3>
<h3>${window.icon('computer', { width: 16, height: 16, class: 'icon', strokeWidth: 2 })} Node Resource Details</h3>
<div class="nodes-grid" data-item-count="${nodeCount}">
${nodesHtml}
</div>
@@ -378,7 +378,7 @@ class MonitoringViewComponent extends Component {
<div class="node-header">
<div class="status-hostname-group">
<div class="node-status-indicator status-offline">
🔴
${window.icon('dotRed', { width: 12, height: 12 })}
</div>
<div class="node-title">${hostname || ip}</div>
</div>
@@ -397,12 +397,12 @@ class MonitoringViewComponent extends Component {
` : ''}
<div class="node-error">
<div class="error-label">⚠️ Error</div>
<div class="error-label">${window.icon('warning', { width: 14, height: 14 })} Error</div>
<div class="error-message">${error || 'Monitoring endpoint not available'}</div>
</div>
${nodeData.lastSeen ? `
<div class="node-uptime">
<div class="uptime-label">⏱️ Last Seen</div>
<div class="uptime-label">${window.icon('timer', { width: 14, height: 14 })} Last Seen</div>
<div class="uptime-value">${this.formatLastSeen(nodeData.lastSeen)}</div>
</div>
` : ''}
@@ -457,8 +457,8 @@ class MonitoringViewComponent extends Component {
}
// Determine status indicator based on resource source
const statusIcon = resourceSource === 'monitoring' ? '🟢' :
resourceSource === 'basic' ? '🟡' : '🔴';
const statusIcon = resourceSource === 'monitoring' ? window.icon('dotGreen', { width: 12, height: 12 }) :
resourceSource === 'basic' ? window.icon('dotYellow', { width: 12, height: 12 }) : window.icon('dotRed', { width: 12, height: 12 });
const statusClass = resourceSource === 'monitoring' ? 'status-online' :
resourceSource === 'basic' ? 'status-warning' : 'status-offline';
@@ -487,27 +487,27 @@ class MonitoringViewComponent extends Component {
${system.uptime_formatted ? `
<div class="node-uptime">
<div class="uptime-label">⏱️ Uptime</div>
<div class="uptime-label">${window.icon('timer', { width: 14, height: 14 })} Uptime</div>
<div class="uptime-value">${system.uptime_formatted}</div>
</div>
` : ''}
<div class="node-latency">
<div class="latency-label">🐢 Latency</div>
<div class="latency-label">${window.icon('latency', { width: 14, height: 14 })} Latency</div>
<div class="latency-value">${nodeData.latency ? `${nodeData.latency}ms` : 'N/A'}</div>
</div>
<div class="latency-divider"></div>
${(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize) ? `
<div class="node-flash">
<div class="flash-label">💾 Flash</div>
<div class="flash-label">${window.icon('storage', { width: 14, height: 14 })} Flash</div>
<div class="flash-value">${this.formatFlashSize(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize)}</div>
</div>
` : ''}
<div class="node-resources">
<div class="resource-item">
<div class="resource-label"> CPU</div>
<div class="resource-label">${window.icon('cpu', { width: 14, height: 14 })} CPU</div>
<div class="resource-value">
<span class="value-label">Used:</span> ${Math.round(cpuUsed)}MHz / <span class="value-label">Total:</span> ${Math.round(cpuTotal)}MHz
</div>
@@ -520,7 +520,7 @@ class MonitoringViewComponent extends Component {
</div>
<div class="resource-item">
<div class="resource-label">🧠 Memory</div>
<div class="resource-label">${window.icon('memory', { width: 14, height: 14 })} Memory</div>
<div class="resource-value">
<span class="value-label">Used:</span> ${this.viewModel.formatResourceValue(memoryUsed, 'memory')} / <span class="value-label">Total:</span> ${this.viewModel.formatResourceValue(memoryTotal, 'memory')}
</div>
@@ -533,7 +533,7 @@ class MonitoringViewComponent extends Component {
</div>
<div class="resource-item">
<div class="resource-label">💾 Storage</div>
<div class="resource-label">${window.icon('storage', { width: 14, height: 14 })} Storage</div>
<div class="resource-value">
<span class="value-label">Used:</span> ${this.viewModel.formatResourceValue(storageUsed, 'storage')} / <span class="value-label">Total:</span> ${this.viewModel.formatResourceValue(storageTotal, 'storage')}
</div>

View File

@@ -516,7 +516,7 @@ class NodeDetailsComponent extends Component {
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
return `
<div class="no-endpoints">
<div>🧩 No endpoints 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 endpoints</div>
</div>
`;
@@ -668,14 +668,14 @@ class NodeDetailsComponent extends Component {
const pretty = typeof response?.data === 'object' ? JSON.stringify(response.data, null, 2) : String(response?.data ?? '');
resultEl.innerHTML = `
<div class="endpoint-call-success">
<div> Success</div>
<div>${window.icon('success', { width: 14, height: 14 })} Success</div>
<pre class="endpoint-result-pre">${this.escapeHtml(pretty)}</pre>
</div>
`;
} catch (err) {
resultEl.innerHTML = `
<div class="endpoint-call-error">
<div> Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
<div>${window.icon('error', { width: 14, height: 14 })} Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
</div>
`;
}
@@ -733,7 +733,7 @@ class NodeDetailsComponent extends Component {
const summaryHTML = summary ? `
<div class="tasks-summary">
<div class="tasks-summary-left">
<div class="summary-icon">📋</div>
<div class="summary-icon">${window.icon('file', { width: 16, height: 16 })}</div>
<div>
<div class="summary-title">Tasks Overview</div>
<div class="summary-subtitle">System task management and monitoring</div>
@@ -760,12 +760,12 @@ class NodeDetailsComponent extends Component {
<div class="task-header">
<span class="task-name">${task.name || 'Unknown Task'}</span>
<span class="task-status ${task.running ? 'running' : 'stopped'}">
${task.running ? '🟢 Running' : '🔴 Stopped'}
${task.running ? window.icon('dotGreen', { width: 10, height: 10 }) + ' Running' : window.icon('dotRed', { width: 10, height: 10 }) + ' Stopped'}
</span>
</div>
<div class="task-details">
<span class="task-interval">Interval: ${task.interval}ms</span>
<span class="task-enabled">${task.enabled ? '🟢 Enabled' : '🔴 Disabled'}</span>
<span class="task-enabled">${task.enabled ? window.icon('dotGreen', { width: 10, height: 10 }) + ' Enabled' : window.icon('dotRed', { width: 10, height: 10 }) + ' Disabled'}</span>
</div>
</div>
`).join('');
@@ -780,7 +780,7 @@ class NodeDetailsComponent extends Component {
return `
<div class="tasks-summary">
<div class="tasks-summary-left">
<div class="summary-icon">📋</div>
<div class="summary-icon">${window.icon('file', { width: 16, height: 16 })}</div>
<div>
<div class="summary-title">Tasks Overview</div>
<div class="summary-subtitle">${total > 0 ? `Total tasks: ${total}, active: ${active}` : 'This node has no running tasks'}</div>
@@ -802,7 +802,7 @@ class NodeDetailsComponent extends Component {
</div>
</div>
<div class="no-tasks">
<div>📋 No active tasks found</div>
<div>No active tasks found</div>
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">
${total > 0 ? `Total tasks: ${total}, active: ${active}` : 'This node has no running tasks'}
</div>
@@ -818,7 +818,7 @@ class NodeDetailsComponent extends Component {
<div class="upload-area">
<input type="file" id="firmware-file" accept=".bin,.hex" style="display: none;">
<button class="upload-btn" data-action="select-file">
📁 Choose Firmware File
${window.icon('file', { width: 14, height: 14 })} Choose Firmware File
</button>
<div class="upload-info">Select a .bin or .hex file to upload</div>
<div id="upload-status" style="display: none;"></div>
@@ -879,15 +879,15 @@ class NodeDetailsComponent extends Component {
// Show upload status
uploadStatus.style.display = 'block';
uploadStatus.innerHTML = `
<div class="upload-progress">
<div>📤 Uploading ${file.name}...</div>
<div class="upload-progress">
<div>${window.icon('upload', { width: 14, height: 14 })} Uploading ${this.escapeHtml(file.name)}...</div>
<div style="font-size: 0.8rem; opacity: 0.7;">Size: ${(file.size / 1024).toFixed(1)}KB</div>
</div>
`;
// Disable upload button
uploadBtn.disabled = true;
uploadBtn.textContent = 'Uploading...';
uploadBtn.textContent = 'Uploading...';
// Get the member IP from the card if available, otherwise fallback to view model state
const memberCard = this.container.closest('.member-card');
@@ -908,7 +908,7 @@ class NodeDetailsComponent extends Component {
// Show success
uploadStatus.innerHTML = `
<div class="upload-success">
<div> Firmware uploaded successfully!</div>
<div>${window.icon('success', { width: 14, height: 14 })} Firmware uploaded successfully!</div>
<div style="font-size: 0.8rem; margin-top: 0.5rem; opacity: 0.7;">Node: ${memberIp}</div>
<div style="font-size: 0.8rem; margin-top: 0.5rem; opacity: 0.7;">Size: ${(file.size / 1024).toFixed(1)}KB</div>
</div>
@@ -922,7 +922,7 @@ class NodeDetailsComponent extends Component {
// Show error
uploadStatus.innerHTML = `
<div class="upload-error">
<div>❌ Upload failed: ${error.message}</div>
<div>${window.icon('error', { width: 14, height: 14 })} Upload failed: ${this.escapeHtml(error.message)}</div>
</div>
`;
} finally {

View File

@@ -28,7 +28,7 @@ class PrimaryNodeComponent extends Component {
const error = this.viewModel.get('error');
if (error) {
this.setText('#primary-node-ip', 'Discovery Failed');
this.setText('#primary-node-ip', 'Discovery Failed');
this.setClass('#primary-node-ip', 'error', true);
this.setClass('#primary-node-ip', 'discovering', false);
this.setClass('#primary-node-ip', 'selecting', false);
@@ -36,19 +36,19 @@ class PrimaryNodeComponent extends Component {
}
if (!primaryNode) {
this.setText('#primary-node-ip', '🔍 No Nodes Found');
this.setText('#primary-node-ip', 'No Nodes Found');
this.setClass('#primary-node-ip', 'error', true);
this.setClass('#primary-node-ip', 'discovering', false);
this.setClass('#primary-node-ip', 'selecting', false);
return;
}
const status = clientInitialized ? '' : '⚠️';
const status = clientInitialized ? '' : '';
const nodeCount = (onlineNodes && onlineNodes > 0)
? ` (${onlineNodes}/${totalNodes} online)`
: (totalNodes > 1 ? ` (${totalNodes} nodes)` : '');
this.setText('#primary-node-ip', `${status} ${primaryNode}${nodeCount}`);
this.setText('#primary-node-ip', `${primaryNode}${nodeCount}`);
this.setClass('#primary-node-ip', 'error', false);
this.setClass('#primary-node-ip', 'discovering', false);
this.setClass('#primary-node-ip', 'selecting', false);
@@ -57,7 +57,7 @@ class PrimaryNodeComponent extends Component {
async handleRandomSelection() {
try {
// Show selecting state
this.setText('#primary-node-ip', '🎲 Selecting...');
this.setText('#primary-node-ip', 'Selecting...');
this.setClass('#primary-node-ip', 'selecting', true);
this.setClass('#primary-node-ip', 'discovering', false);
this.setClass('#primary-node-ip', 'error', false);
@@ -65,7 +65,7 @@ class PrimaryNodeComponent extends Component {
await this.viewModel.selectRandomPrimaryNode();
// Show success briefly
this.setText('#primary-node-ip', '🎯 Selection Complete');
this.setText('#primary-node-ip', 'Selection Complete');
// Update display after delay
setTimeout(() => {
@@ -74,7 +74,7 @@ class PrimaryNodeComponent extends Component {
} catch (error) {
logger.error('Failed to select random primary node:', error);
this.setText('#primary-node-ip', 'Selection Failed');
this.setText('#primary-node-ip', 'Selection Failed');
this.setClass('#primary-node-ip', 'error', true);
this.setClass('#primary-node-ip', 'selecting', false);
this.setClass('#primary-node-ip', 'discovering', false);

View File

@@ -103,8 +103,16 @@
<div class="terminal-header">
<div class="terminal-title">Terminal</div>
<div class="terminal-actions">
<button class="terminal-minimize-btn" title="Minimize">_</button>
<button class="terminal-close-btn" title="Close">✕</button>
<button class="terminal-minimize-btn" title="Minimize" aria-label="Minimize">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<path d="M5 19h14"/>
</svg>
</button>
<button class="terminal-close-btn" title="Close" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<div class="terminal-body">

61
public/scripts/icons.js Normal file
View File

@@ -0,0 +1,61 @@
// Centralized SVG Icons for SPORE UI
// Usage: window.icon('cluster', {class: 'foo', width: 16, height: 16}) -> returns inline SVG string
(function(){
const toAttrs = (opts) => {
if (!opts) return '';
const attrs = [];
if (opts.class) attrs.push(`class="${opts.class}"`);
if (opts.width) attrs.push(`width="${opts.width}"`);
if (opts.height) attrs.push(`height="${opts.height}"`);
if (opts.strokeWidth) attrs.push(`stroke-width="${opts.strokeWidth}"`);
return attrs.join(' ');
};
const withSvg = (inner, opts) => {
const attr = toAttrs(opts);
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ${attr}>${inner}</svg>`;
};
const Icons = {
// Navigation / sections
cluster: (o) => withSvg(`<circle cx="12" cy="12" r="9"/><circle cx="8" cy="10" r="1.5"/><circle cx="16" cy="8" r="1.5"/><circle cx="14" cy="15" r="1.5"/><path d="M9 11l3 3M9 11l6-3"/>`, o),
topology: (o) => withSvg(`<path d="M6 3v6M6 15v6M18 3v6M18 15v6"/><rect x="3" y="9" width="6" height="6" rx="2"/><rect x="15" y="9" width="6" height="6" rx="2"/>`, o),
monitoring: (o) => withSvg(`<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>`, o),
firmware: (o) => withSvg(`<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/><path d="M12 8v8"/>`, o),
// Status / feedback
success: (o) => withSvg(`<path d="M20 6L9 17l-5-5"/>`, o),
warning: (o) => withSvg(`<path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L14.71 3.86a2 2 0 0 0-3.42 0z"/>`, o),
error: (o) => withSvg(`<path d="M18 6L6 18M6 6l12 12"/>`, o),
offlineDot: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="currentColor"/>`, o),
dotGreen: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="#10b981"/>`, o),
dotYellow: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="#f59e0b"/>`, o),
dotRed: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="#ef4444"/>`, o),
// Actions
refresh: (o) => withSvg(`<path d="M3 3v6h6"/><path d="M21 21v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L3 9m18 6-2.64 2.36A9 9 0 0 1 3.51 15"/>`, o),
terminal: (o) => withSvg(`<path d="M4 17l6-6-6-6"></path><path d="M12 19h8"></path>`, o),
chevronDown: (o) => withSvg(`<path d="M6 9l6 6 6-6"/>`, o),
upload: (o) => withSvg(`<path d="M12 16V4"/><path d="M8 8l4-4 4 4"/><path d="M20 16v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/>`, o),
file: (o) => withSvg(`<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>`, o),
timer: (o) => withSvg(`<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/>`, o),
cpu: (o) => withSvg(`<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/>`, o),
memory: (o) => withSvg(`<rect x="4" y="8" width="16" height="8" rx="2"/><path d="M7 8v8M12 8v8M17 8v8"/>`, o),
storage: (o) => withSvg(`<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M7 10h10"/>`, o),
computer: (o) => withSvg(`<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8"/>`, o),
latency: (o) => withSvg(`<path d="M3 12h4l2 5 4-10 2 7 2-4h4"/>`, o)
};
function icon(name, opts){
const fn = Icons[name];
if (!fn) return '';
return fn(Object.assign({ width: 16, height: 16, strokeWidth: 2 }, opts || {}));
}
if (typeof window !== 'undefined') {
window.Icons = Icons;
window.icon = icon;
}
})();

View File

@@ -4,6 +4,35 @@
box-sizing: border-box;
}
/* === SVG icon alignment and sizing tweaks === */
/* Align inline SVGs vertically with adjacent text */
.nav-tab > svg,
.theme-toggle > svg,
.burger-btn > svg,
h2 > svg,
h3 > svg,
.stat-icon > svg,
.resource-label > svg,
.flash-label > svg,
.uptime-label > svg,
.latency-label > svg,
.member-status > svg,
.tab-refresh-btn > svg,
.primary-node-info svg,
.upload-btn-compact > svg,
.deploy-btn > svg,
.upload-btn > svg,
.terminal-actions svg,
.terminal-dock-icon > svg {
vertical-align: middle;
}
/* Monitoring view: slightly larger summary stat icons */
.summary-stats .stat-icon > svg {
width: 22px;
height: 22px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg-primary);
@@ -767,16 +796,17 @@ p {
flex-shrink: 0;
}
.member-status {
.member-status,
.node-status-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.2rem;
width: 1rem;
height: 1rem;
padding: 0;
border-radius: 50%;
font-size: 0.9rem;
font-size: 0; /* prevent line-height affecting size */
flex-shrink: 0;
min-width: 1.2rem;
min-height: 1.2rem;
}
.status-online {
@@ -790,12 +820,22 @@ p {
background: rgba(244, 67, 54, 0.3);
color: #f44336;
border: 1px solid rgba(244, 67, 54, 0.5);
border-radius: 50%;
}
.status-inactive {
background: rgba(255, 152, 0, 0.3);
color: #ff9800;
border: 1px solid rgba(255, 152, 0, 0.5);
border-radius: 50%;
}
/* Ensure the inner dot SVG fits nicely */
.member-status > svg,
.node-status-indicator > svg {
width: 8px;
height: 8px;
display: block;
}
.member-latency {
@@ -3452,6 +3492,7 @@ select.param-input:focus {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border-secondary);
background: var(--bg-secondary);
border-radius: 12px 12px 0 0;
}
.terminal-title {
font-weight: 600;