diff --git a/public/index.html b/public/index.html index 73cbed0..b54c48b 100644 --- a/public/index.html +++ b/public/index.html @@ -248,6 +248,7 @@ + diff --git a/public/scripts/api-client.js b/public/scripts/api-client.js index 3f9e6ed..6641bf0 100644 --- a/public/scripts/api-client.js +++ b/public/scripts/api-client.js @@ -137,77 +137,40 @@ class ApiClient { }); } - // Registry API methods - async getRegistryBaseUrl() { - // Auto-detect registry server URL based on current location - const currentHost = window.location.hostname; - - // If accessing from localhost, use localhost:8080 - // If accessing from another device, use the same hostname but port 8080 - if (currentHost === 'localhost' || currentHost === '127.0.0.1') { - return 'http://localhost:8080'; - } else { - return `http://${currentHost}:8080`; - } + // Registry API methods - now proxied through gateway + async getRegistryHealth() { + return this.request('/api/registry/health', { method: 'GET' }); } async uploadFirmwareToRegistry(metadata, firmwareFile) { - const registryBaseUrl = await this.getRegistryBaseUrl(); const formData = new FormData(); formData.append('metadata', JSON.stringify(metadata)); formData.append('firmware', firmwareFile); - const response = await fetch(`${registryBaseUrl}/firmware`, { + return this.request('/api/registry/firmware', { method: 'POST', body: formData }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Registry upload failed: ${errorText}`); - } - - return await response.json(); } async updateFirmwareMetadata(name, version, metadata) { - const registryBaseUrl = await this.getRegistryBaseUrl(); - - const response = await fetch(`${registryBaseUrl}/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { + return this.request(`/api/registry/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(metadata) + body: metadata }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Registry metadata update failed: ${errorText}`); - } - - return await response.json(); } async listFirmwareFromRegistry(name = null, version = null) { - const registryBaseUrl = await this.getRegistryBaseUrl(); const query = {}; if (name) query.name = name; if (version) query.version = version; - const response = await fetch(`${registryBaseUrl}/firmware${Object.keys(query).length ? '?' + new URLSearchParams(query).toString() : ''}`); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Registry list failed: ${errorText}`); - } - - return await response.json(); + const queryString = Object.keys(query).length ? '?' + new URLSearchParams(query).toString() : ''; + return this.request(`/api/registry/firmware${queryString}`, { method: 'GET' }); } async downloadFirmwareFromRegistry(name, version) { - const registryBaseUrl = await this.getRegistryBaseUrl(); - const response = await fetch(`${registryBaseUrl}/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`); + const response = await fetch(`${this.baseURL}/api/registry/firmware/${encodeURIComponent(name)}/${encodeURIComponent(version)}`); if (!response.ok) { const errorText = await response.text(); @@ -217,16 +180,16 @@ class ApiClient { return response.blob(); } - async getRegistryHealth() { - const registryBaseUrl = await this.getRegistryBaseUrl(); - const response = await fetch(`${registryBaseUrl}/health`); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Registry health check failed: ${errorText}`); - } - - return await response.json(); + // Rollout API methods + async getClusterNodeVersions() { + return this.request('/api/cluster/node/versions', { method: 'GET' }); + } + + async startRollout(rolloutData) { + return this.request('/api/rollout', { + method: 'POST', + body: rolloutData + }); } } @@ -320,6 +283,9 @@ class WebSocketClient { case 'firmware_upload_status': this.emit('firmwareUploadStatus', data); break; + case 'rollout_progress': + this.emit('rolloutProgress', data); + break; default: logger.debug('Unknown WebSocket message type:', data.type); } diff --git a/public/scripts/components/FirmwareComponent.js b/public/scripts/components/FirmwareComponent.js index be55d50..5978bde 100644 --- a/public/scripts/components/FirmwareComponent.js +++ b/public/scripts/components/FirmwareComponent.js @@ -168,8 +168,13 @@ class FirmwareComponent extends Component { return `
-

${this.escapeHtml(group.name)}

- ${group.firmware.length} version${group.firmware.length !== 1 ? 's' : ''} +
+

${this.escapeHtml(group.name)}

+ ${group.firmware.length} version${group.firmware.length !== 1 ? 's' : ''} +
+ + +
${versionsHTML} @@ -198,6 +203,13 @@ class FirmwareComponent extends Component {
+ + +
+ + `; + + this.setupEventListeners(); + } + + handleRollout() { + if (!this.onRolloutCallback || this.matchingNodes.length === 0) { + return; + } + + // Send the firmware info and matching nodes directly + const rolloutData = { + firmware: { + name: this.rolloutData.name, + version: this.rolloutData.version, + labels: this.rolloutData.labels + }, + nodes: this.matchingNodes + }; + + this.onRolloutCallback(rolloutData); + } + + handleCancel() { + if (this.onCancelCallback) { + this.onCancelCallback(); + } + } + + escapeHtml(text) { + if (typeof text !== 'string') return text; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +window.RolloutComponent = RolloutComponent; diff --git a/public/styles/main.css b/public/styles/main.css index 4e12618..4322462 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -701,10 +701,6 @@ p { display: flex; align-items: center; gap: 0.5rem; - padding: 0.75rem; - background: var(--bg-tertiary); - border: 1px solid var(--border-primary); - border-radius: 8px; } .label-key-input, @@ -1954,7 +1950,7 @@ p { background: rgba(244, 67, 54, 0.2); border: 1px solid rgba(244, 67, 54, 0.3); color: #ffcdd2; - padding: 1rem; + /*padding: 1rem;*/ border-radius: 8px; margin-top: 1rem; } @@ -2269,7 +2265,6 @@ p { .firmware-groups { display: flex; flex-direction: column; - gap: 1.5rem; } .firmware-group { @@ -2312,11 +2307,27 @@ p { display: flex; align-items: center; justify-content: space-between; + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + cursor: pointer; + position: relative; + z-index: 2; + user-select: none; +} + +.firmware-group.expanded .firmware-group-header { margin-bottom: 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border-primary); } +.firmware-group-header-content { + display: flex; + align-items: center; + gap: 1rem; +} + .firmware-group-name { margin: 0; font-size: 1.125rem; @@ -2333,10 +2344,23 @@ p { font-weight: 500; } +.firmware-group-chevron { + color: var(--text-secondary); + transition: transform 0.3s ease; + flex-shrink: 0; +} + +.firmware-group.expanded .firmware-group-chevron { + transform: rotate(180deg); +} + .firmware-versions { - display: flex; + display: none; flex-direction: column; - gap: 0.75rem; +} + +.firmware-group.expanded .firmware-versions { + display: flex; } .firmware-version-item { @@ -2594,9 +2618,9 @@ p { } .action-btn.download-btn:hover { - background: rgba(34, 197, 94, 0.1); - border-color: #22c55e; - color: #22c55e; + background: rgba(96, 165, 250, 0.1); + border-color: var(--accent-secondary); + color: var(--accent-secondary); } .action-btn.delete-btn:hover { @@ -4934,27 +4958,32 @@ select.param-input:focus { } .target-nodes-section h3 { - margin: 0 0 1.5rem 0; + margin: 0 0 0.75rem 0; font-size: 1rem; font-weight: 600; color: var(--text-primary); } .target-nodes-list { - display: flex; - flex-direction: column; - gap: 0.5rem; + overflow-y: auto; + border: 1px solid var(--border-primary); + border-radius: 8px; + background: var(--bg-tertiary); } .target-node-item { + padding: 0.75rem; + border-bottom: 1px solid var(--border-primary); display: flex; justify-content: space-between; - align-items: flex-start; - padding: 0.7rem; - background: var(--bg-primary); - border-radius: 12px; - border: 1px solid var(--border-primary); - gap: 1.5rem; + align-items: center; + background: transparent; + border-radius: 0; + gap: 0; +} + +.target-node-item:last-child { + border-bottom: none; } .target-node-item .node-info { @@ -4995,7 +5024,6 @@ select.param-input:focus { font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; - min-width: 100px; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } @@ -6733,7 +6761,7 @@ html { .label-chip { display: inline-flex; align-items: center; - font-size: 0.75rem; + font-size: 0.9rem; padding: 0.25rem 0.5rem; border-radius: 9999px; background: rgba(30, 58, 138, 0.35); @@ -7473,3 +7501,322 @@ html { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); } + +/* === Rollout Component Styles === */ +.rollout-btn { + background: transparent; + border: 1px solid var(--border-secondary); + color: var(--text-secondary); + border-radius: 6px; + padding: 0.375rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.rollout-btn:hover { + background: rgba(34, 197, 94, 0.1); + border-color: #22c55e; + color: #22c55e; +} + +.rollout-btn:active { + transform: translateY(0); +} + +.rollout-panel { + padding: 1.5rem; + max-width: 600px; +} + +.rollout-header { + margin-bottom: 1.5rem; +} + +.rollout-header h3 { + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.rollout-header p { + color: var(--text-secondary); + margin-bottom: 0; +} + +.rollout-firmware-info { + background: var(--bg-tertiary); + border-radius: 8px; + padding: 1rem; + margin-bottom: 1.5rem; + border: 1px solid var(--border-primary); +} + +.rollout-firmware-name { + font-size: 1.2rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.rollout-firmware-version { + color: var(--text-secondary); + margin-bottom: 0.75rem; +} + +.rollout-firmware-labels { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.rollout-matching-nodes { + margin-bottom: 1.5rem; +} + +.rollout-matching-nodes h4 { + margin-bottom: 0.75rem; + color: var(--text-primary); +} + +.rollout-nodes-list { + max-height: 200px; + overflow-y: auto; + border: 1px solid var(--border-primary); + border-radius: 8px; + background: var(--bg-tertiary); +} + +.rollout-node-item { + padding: 0.75rem; + border-bottom: 1px solid var(--border-primary); + display: flex; + justify-content: space-between; + align-items: center; +} + +.rollout-node-item:last-child { + border-bottom: none; +} + +.rollout-node-info { + flex: 1; +} + +.rollout-node-status { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; + min-width: 140px; + text-align: right; +} + +.rollout-node-ip { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.25rem; +} + +.rollout-node-version { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.rollout-node-labels { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.label-chip.small { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; +} + +.rollout-warning { + display: flex; + align-items: flex-start; + gap: 0.75rem; + background: var(--warning-bg); + border: 1px solid var(--warning-border); + border-radius: 8px; + padding: 1rem; + margin-bottom: 1.5rem; +} + +.warning-icon { + color: var(--warning-color); + flex-shrink: 0; + margin-top: 0.125rem; +} + +.warning-text { + color: var(--accent-warning); + line-height: 1.5; +} + +.rollout-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + padding-top: 1rem; + border-top: 1px solid var(--border-primary); +} + +/* Rollout confirm button - make it look important */ +.rollout-actions .deploy-btn { + background: linear-gradient(135deg, rgba(74, 222, 128, 0.2) 0%, rgba(74, 222, 128, 0.1) 100%); + border: 1px solid rgba(74, 222, 128, 0.3); + color: #41cb6d; + font-weight: 500; + padding: 0.75rem 1.25rem; + font-size: 0.9rem; + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.rollout-actions .deploy-btn:hover { + background: linear-gradient(135deg, rgba(74, 222, 128, 0.3) 0%, rgba(74, 222, 128, 0.15) 100%); + border-color: rgba(74, 222, 128, 0.4); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(74, 222, 128, 0.2); +} + +.rollout-actions .deploy-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +/* Rollout Progress Overlay */ +.rollout-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.rollout-progress-overlay { + background: var(--bg-secondary); + border-radius: 16px; + border: 1px solid var(--border-primary); + box-shadow: var(--shadow-primary); + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow: hidden; +} + +.rollout-progress-content { + padding: 2rem; +} + +.rollout-progress-header { + text-align: center; + margin-bottom: 1.5rem; +} + +.rollout-progress-header h3 { + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.rollout-progress-body { + text-align: center; +} + +.rollout-progress-info { + margin-bottom: 1.5rem; +} + +.rollout-progress-info p { + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.rollout-progress-text { + font-weight: 600; + color: var(--text-primary); +} + +.rollout-progress-bar { + width: 100%; + height: 8px; + background: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; + margin-bottom: 1.5rem; +} + +.rollout-progress-fill { + height: 100%; + background: var(--accent-primary); + border-radius: 4px; + transition: width 0.3s ease; + width: 0%; +} + +.rollout-progress-details { + text-align: left; +} + +.rollout-node-list { + overflow-y: auto; + border: 1px solid var(--border-primary); + border-radius: 8px; + background: var(--bg-tertiary); +} + +.rollout-node-item { + padding: 0.75rem; + border-bottom: 1px solid var(--border-primary); + display: flex; + justify-content: space-between; + align-items: center; +} + +.rollout-node-item:last-child { + border-bottom: none; +} + +.rollout-node-ip { + font-weight: 600; + color: var(--text-primary); +} + +.rollout-node-status { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 600; +} + +.rollout-node-status.status-updating_labels { + background: var(--warning-bg); + color: var(--warning-color); +} + +.rollout-node-status.status-uploading { + background: var(--info-bg); + color: var(--info-color); +} + +.rollout-node-status.status-completed { + background: var(--success-bg); + color: var(--success-color); +} + +.rollout-node-status.status-failed { + background: var(--error-bg); + color: var(--error-color); +} diff --git a/public/styles/theme.css b/public/styles/theme.css index 7ad138b..1e993cf 100644 --- a/public/styles/theme.css +++ b/public/styles/theme.css @@ -16,7 +16,7 @@ --border-secondary: rgba(255, 255, 255, 0.15); --border-hover: rgba(255, 255, 255, 0.2); - --accent-primary: #4ade80; + --accent-primary: #1d8b45; --accent-secondary: #60a5fa; --accent-warning: #fbbf24; --accent-error: #f87171;