From f4cf3d0b6a19a6f2e469ede5294c5cb8503d81ca Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Wed, 2 Sep 2026 19:41:39 -0400 Subject: [PATCH] add agent binary upload and previous-versions UI Surface versioned agent/tray uploads, current-version display, and an expandable retained-version list with rollback in the Build Site Agent card, consuming the new siteagent status fields. --- .../frontend/views/AgentKeySettings.vue | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/plugins/endpoints/frontend/views/AgentKeySettings.vue b/plugins/endpoints/frontend/views/AgentKeySettings.vue index e3816c1e..51d623d9 100644 --- a/plugins/endpoints/frontend/views/AgentKeySettings.vue +++ b/plugins/endpoints/frontend/views/AgentKeySettings.vue @@ -303,6 +303,90 @@

{{ agentmessage }}

{{ agenterror }}

+ + +
+

{{ binary.label }}

+

+ Current staged version: + {{ binary.current || 'none (legacy or unstaged)' }} +

+ +
+ + + +
+

{{ binary.message }}

+

{{ binary.error }}

+ +
+ + + + + + + + + + + + + + + + + + +
VersionUploadedSize
{{ entry.version }}{{ formatDate(entry.uploadedat) }}{{ formatBytes(entry.size) }} + current + +
+
+
@@ -370,6 +454,29 @@ const agentbusy = ref(false) const agentmessage = ref('') const agenterror = ref('') +// Versioned agent/tray binaries. Each block owns its own upload form, retained +// version list, and busy/message state so the two rows act independently. The +// 'key' matches the backend binkey (agent | tray). +const binaries = ref([ + { key: 'agent', label: 'Endpoint agent (shopdbagent.exe)', + current: null, versions: [], version: '', file: null, + busy: false, message: '', error: '', showprevious: false }, + { key: 'tray', label: 'Tray agent (shopdb-endpoint-tray.exe)', + current: null, versions: [], version: '', file: null, + busy: false, message: '', error: '', showprevious: false }, +]) + +function binaryFor(binkey) { + return binaries.value.find((entry) => entry.key === binkey) +} + +function formatBytes(value) { + if (!value && value !== 0) return '' + if (value < 1024) return value + ' B' + if (value < 1024 * 1024) return (value / 1024).toFixed(1) + ' KB' + return (value / (1024 * 1024)).toFixed(1) + ' MB' +} + function formatDate(value) { if (!value) return '' const parsed = new Date(value) @@ -401,11 +508,20 @@ onMounted(async () => { agentstaged.value = !!status.agentstaged certavailable.value = !!status.certavailable agentbundlecert.value = !!status.certavailable + applyBinaryStatus('agent', status.agentcurrent, status.agentversions) + applyBinaryStatus('tray', status.traycurrent, status.trayversions) } catch (statusError) { console.error(statusError) } }) +function applyBinaryStatus(binkey, current, versions) { + const binary = binaryFor(binkey) + if (!binary) return + binary.current = current || null + binary.versions = Array.isArray(versions) ? versions : [] +} + async function regenerate() { if (key.value && !window.confirm('Regenerate the key? The current key stops working ' @@ -548,6 +664,66 @@ async function buildSiteAgent() { } } +function onBinaryFileChange(binkey, event) { + const binary = binaryFor(binkey) + if (!binary) return + const files = event.target.files + binary.file = files && files.length ? files[0] : null +} + +async function uploadBinary(binkey) { + const binary = binaryFor(binkey) + if (!binary || !binary.file || !binary.version) return + binary.busy = true + binary.message = '' + binary.error = '' + try { + const form = new FormData() + form.append('version', binary.version) + form.append('file', binary.file) + const response = await api.post( + `/endpoints/siteagent/binaries/${binkey}`, form) + const data = response.data.data || {} + applyBinaryStatus(binkey, data.current, data.versions) + binary.version = '' + binary.file = null + binary.message = 'Uploaded and set as current.' + } catch (uploadError) { + binary.error = uploadError.response?.data?.data?.error?.message + || (uploadError.response?.status === 403 + ? 'You do not have permission to upload agent binaries' + : 'Could not upload the binary') + console.error(uploadError) + } finally { + binary.busy = false + } +} + +async function setCurrentBinary(binkey, version) { + const binary = binaryFor(binkey) + if (!binary) return + if (!window.confirm(`Make version ${version} the current staged build? ` + + 'New downloads will ship this version.')) { + return + } + binary.busy = true + binary.message = '' + binary.error = '' + try { + const response = await api.put( + `/endpoints/siteagent/binaries/${binkey}/current`, { version }) + const data = response.data.data || {} + applyBinaryStatus(binkey, data.current, data.versions) + binary.message = `Version ${version} is now current.` + } catch (setError) { + binary.error = setError.response?.data?.data?.error?.message + || 'Could not change the current version' + console.error(setError) + } finally { + binary.busy = false + } +} + function editConfiguration() { router.push({ name: 'endpoints-monitors-settings' }) } @@ -585,4 +761,45 @@ function editConfiguration() { .settings-section > h3 { margin-bottom: 0.25rem; } .info-list { margin: 0.25rem 0 0; padding-left: 1.25rem; } .info-list li { margin-bottom: 0.5rem; } +.binary-block { + margin-top: 1.25rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border, #333); +} +.binary-upload { + display: flex; + gap: 0.5rem; + align-items: center; + flex-wrap: wrap; + margin: 0.5rem 0; +} +.binary-upload .form-control[type="text"] { max-width: 12rem; } +.previous-versions { margin-top: 0.5rem; } +.btn-link { + background: none; + border: none; + color: var(--color-link, #4ea1ff); + cursor: pointer; + padding: 0; + font-size: 0.9rem; + text-decoration: underline; +} +.btn-link:disabled { color: var(--color-text-muted, #888); cursor: default; } +.versions-table { + width: 100%; + margin-top: 0.5rem; + border-collapse: collapse; + font-size: 0.85rem; +} +.versions-table th, +.versions-table td { + text-align: left; + padding: 0.3rem 0.5rem; + border-bottom: 1px solid var(--color-border, #333); +} +.current-tag { + font-size: 0.8rem; + color: var(--color-success, #4caf50); + font-weight: 600; +}