From cf43a7ab14e7567b867191cfe212bdc96a8b2eda Mon Sep 17 00:00:00 2001 From: Gary Date: Thu, 8 Oct 2026 19:49:22 +0200 Subject: [PATCH 1/2] feat(packages): show Docker on a line of its own in System packages The core now leaves Docker's packages out of the update and reports them separately: the modal shows Docker's version, where it came from, what it has on offer, a warning that updating it restarts it and stops every container, and the command to type to do it by hand. The confirmation says Docker is not part of the update. --- src/assets/lang/en_US.json | 14 +++- src/assets/lang/fr_FR.json | 14 +++- .../settings/SystemPackageUpdateModal.spec.js | 80 ++++++++++++++++++- .../settings/SystemPackageUpdateModal.vue | 70 +++++++++++++++- 4 files changed, 172 insertions(+), 6 deletions(-) diff --git a/src/assets/lang/en_US.json b/src/assets/lang/en_US.json index cdf3b5d8..01282fc2 100644 --- a/src/assets/lang/en_US.json +++ b/src/assets/lang/en_US.json @@ -928,5 +928,17 @@ "ReCasaOS update": "ReCasaOS update", "Share on {network}": "Share on {network}", "The update could not be started.": "The update could not be started.", - "The progress of the update could not be loaded.": "The progress of the update could not be loaded." + "The progress of the update could not be loaded.": "The progress of the update could not be loaded.", + "Docker": "Docker", + "Docker has updates. This update leaves it alone.": "Docker has updates. This update leaves it alone.", + "Updating Docker restarts it: every container stops until it is back. Apps set to restart start again by themselves, the others stay stopped. Do it when that suits you.": "Updating Docker restarts it: every container stops until it is back. Apps set to restart start again by themselves, the others stay stopped. Do it when that suits you.", + "In a terminal on this machine:": "In a terminal on this machine:", + "ReCasaOS does not know how Docker was installed here, so it shows no command.": "ReCasaOS does not know how Docker was installed here, so it shows no command.", + "Docker is up to date.": "Docker is up to date.", + "Docker is not installed from a package here.": "Docker is not installed from a package here.", + "Installed from Docker's own repository.": "Installed from Docker's own repository.", + "Installed from your distribution's docker.io package.": "Installed from your distribution's docker.io package.", + "Installed as a snap.": "Installed as a snap.", + "Installed from a source ReCasaOS does not recognise.": "Installed from a source ReCasaOS does not recognise.", + "Are you sure you want to update the system packages listed? Docker is not part of this update.": "Are you sure you want to update the system packages listed? Docker is not part of this update." } diff --git a/src/assets/lang/fr_FR.json b/src/assets/lang/fr_FR.json index 7bf63680..2a54ad05 100644 --- a/src/assets/lang/fr_FR.json +++ b/src/assets/lang/fr_FR.json @@ -928,5 +928,17 @@ "ReCasaOS update": "Mise à jour de ReCasaOS", "Share on {network}": "Partager sur {network}", "The update could not be started.": "La mise à jour n’a pas pu démarrer.", - "The progress of the update could not be loaded.": "L’avancement de la mise à jour n’a pas pu être chargé." + "The progress of the update could not be loaded.": "L’avancement de la mise à jour n’a pas pu être chargé.", + "Docker": "Docker", + "Docker has updates. This update leaves it alone.": "Docker a des mises à jour. Cette mise à jour n'y touche pas.", + "Updating Docker restarts it: every container stops until it is back. Apps set to restart start again by themselves, the others stay stopped. Do it when that suits you.": "Mettre Docker à jour le redémarre : tous les conteneurs s'arrêtent jusqu'à son retour. Les apps configurées pour redémarrer repartent d'elles-mêmes, les autres restent arrêtées. Faites-le quand cela vous arrange.", + "In a terminal on this machine:": "Dans un terminal sur cette machine :", + "ReCasaOS does not know how Docker was installed here, so it shows no command.": "ReCasaOS ne sait pas comment Docker a été installé ici, il n'affiche donc pas de commande.", + "Docker is up to date.": "Docker est à jour.", + "Docker is not installed from a package here.": "Docker n'est pas installé depuis un paquet ici.", + "Installed from Docker's own repository.": "Installé depuis le dépôt officiel de Docker.", + "Installed from your distribution's docker.io package.": "Installé depuis le paquet docker.io de votre distribution.", + "Installed as a snap.": "Installé en snap.", + "Installed from a source ReCasaOS does not recognise.": "Installé depuis une source que ReCasaOS ne reconnaît pas.", + "Are you sure you want to update the system packages listed? Docker is not part of this update.": "Voulez-vous vraiment mettre à jour les paquets système listés ? Docker ne fait pas partie de cette mise à jour." } diff --git a/src/components/settings/SystemPackageUpdateModal.spec.js b/src/components/settings/SystemPackageUpdateModal.spec.js index 4d5f317e..10f3c6bc 100644 --- a/src/components/settings/SystemPackageUpdateModal.spec.js +++ b/src/components/settings/SystemPackageUpdateModal.spec.js @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { h } from 'vue' import { flushPromises, mount } from '@vue/test-utils' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import SystemPackageUpdateModal from './SystemPackageUpdateModal.vue' const answer = data => () => Promise.resolve({ data: { success: 200, data } }) @@ -17,10 +17,10 @@ const withSlot = { let wrapper afterEach(() => wrapper?.unmount()) -async function open(sys) { +async function open(sys, mocks = {}) { wrapper = mount(SystemPackageUpdateModal, { global: { - mocks: { $t: key => key, $api: { sys: { getSystemPackageUpdateStatus: answer(idle), ...sys } } }, + mocks: { $t: key => key, $api: { sys: { getSystemPackageUpdateStatus: answer(idle), ...sys } }, ...mocks }, stubs: { 'b-button': withSlot, 'b-message': withSlot, 'b-icon': true }, }, }) @@ -57,3 +57,77 @@ describe('system packages', () => { expect(w.find('pre').text()).toBe('Reading package lists...') }) }) + +describe('docker on a line of its own', () => { + const docker = { + installed: true, + origin: 'docker-repository', + version: '29.8.1', + updates: [{ name: 'docker-ce', current_version: '5:29.8.1-1', candidate_version: '5:29.8.2-1' }], + manual_command: 'sudo apt-get update && sudo apt-get install --only-upgrade docker-ce docker-ce-cli containerd.io', + } + const check = extra => answer({ + supported: true, + manager: 'apt', + updates: [{ name: 'libc6', current_version: '2.35-1', candidate_version: '2.35-2' }], + count: 1, + ...extra, + }) + + it('shows its version and where it came from, what it has, the warning and the command to type', async () => { + const w = await open({ getSystemPackages: check({ docker }) }) + + const line = w.find('.docker-line') + expect(line.exists()).toBe(true) + expect(line.text()).toContain('Docker') + expect(line.text()).toContain('29.8.1') + expect(line.text()).toContain('Installed from Docker\'s own repository.') + expect(line.text()).toContain('Docker has updates. This update leaves it alone.') + expect(line.text()).toContain('docker-ce') + expect(line.text()).toContain('every container stops until it is back') + expect(line.find('pre.docker-command').text()).toBe(docker.manual_command) + // and Docker's package is not among the ones the button installs + expect(w.find('.package-list').text()).toContain('libc6') + expect(w.find('.package-list').text()).not.toContain('docker-ce') + }) + + it('says it is up to date, with no command, when nothing is on offer', async () => { + const w = await open({ getSystemPackages: check({ docker: { ...docker, updates: [] } }) }) + + expect(w.find('.docker-line').text()).toContain('Docker is up to date.') + expect(w.find('.docker-line pre').exists()).toBe(false) + }) + + it('shows no command for a Docker whose origin it does not know, and says so', async () => { + const w = await open({ getSystemPackages: check({ docker: { ...docker, origin: 'unknown', manual_command: '' } }) }) + + expect(w.find('.docker-line').text()).toContain('Installed from a source ReCasaOS does not recognise.') + expect(w.find('.docker-line').text()).toContain('ReCasaOS does not know how Docker was installed here, so it shows no command.') + expect(w.find('.docker-line pre').exists()).toBe(false) + }) + + it('has no Docker line when the host reports none', async () => { + const w = await open({ getSystemPackages: check({}) }) + + expect(w.find('.docker-line').exists()).toBe(false) + }) + + it('shows Docker even when nothing else is to update, next to the "no updates" sentence', async () => { + const w = await open({ getSystemPackages: check({ updates: [], count: 0, docker }) }) + + expect(w.text()).toContain('No system package updates are available.') + expect(w.find('.docker-line').text()).toContain('docker-ce') + expect(w.text()).not.toContain('Update packages') + }) + + it('asks for confirmation of the listed packages and says Docker is not part of it', async () => { + const confirm = vi.fn() + const w = await open({ getSystemPackages: check({ docker }) }, { $buefy: { dialog: { confirm } } }) + + w.vm.confirmUpdate() + + expect(confirm).toHaveBeenCalledTimes(1) + expect(confirm.mock.calls[0][0].message).toContain('Docker is not part of this update.') + expect(confirm.mock.calls[0][0].message).not.toContain('all available') + }) +}) diff --git a/src/components/settings/SystemPackageUpdateModal.vue b/src/components/settings/SystemPackageUpdateModal.vue index 84954f65..353af2ff 100644 --- a/src/components/settings/SystemPackageUpdateModal.vue +++ b/src/components/settings/SystemPackageUpdateModal.vue @@ -56,6 +56,30 @@ +
+

+ {{ $t('Docker') }}{{ info.docker.version }} +

+

{{ dockerOrigin }}

+ +

{{ $t('Docker is up to date.') }}

+
+

{{ $t(isReconciliationPending ? 'Finishing system package update...' : 'Applying system package updates...') }} @@ -138,6 +162,25 @@ export default { hasUpdates() { return Array.isArray(this.info.updates) && this.info.updates.length > 0 }, + dockerHasUpdates() { + return Array.isArray(this.info.docker?.updates) && this.info.docker.updates.length > 0 + }, + dockerOrigin() { + const docker = this.info.docker + if (!docker?.installed) { + return this.$t('Docker is not installed from a package here.') + } + switch (docker.origin) { + case 'docker-repository': + return this.$t('Installed from Docker\'s own repository.') + case 'distribution': + return this.$t('Installed from your distribution\'s docker.io package.') + case 'snap': + return this.$t('Installed as a snap.') + default: + return this.$t('Installed from a source ReCasaOS does not recognise.') + } + }, }, mounted() { this.loadInitialState() @@ -182,7 +225,7 @@ export default { confirmUpdate() { this.$buefy.dialog.confirm({ title: this.$t('Update system packages'), - message: `${this.$t('Are you sure you want to update all available system packages?')}

${this.$t('The package list may change before the update starts.')}`, + message: `${this.$t('Are you sure you want to update the system packages listed? Docker is not part of this update.')}

${this.$t('The package list may change before the update starts.')}`, type: 'is-warning', hasIcon: true, confirmText: this.$t('Update packages'), @@ -268,6 +311,31 @@ export default { } } +.docker-line { + padding-top: 1rem; + border-top: 1px solid rgba(128, 128, 128, 0.25); +} + +.docker-title { + font-weight: 600; +} + +.docker-updates { + margin: 0.25rem 0 0 1rem; + list-style: disc; +} + +.docker-command { + margin: 0.25rem 0 0; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + background: rgba(128, 128, 128, 0.12); + white-space: pre-wrap; + overflow-wrap: anywhere; + user-select: all; + font-size: 0.75rem; +} + .package-log { max-height: 14rem; overflow: auto; From 9f8b9621034faf3957c1df1b962334de65744365 Mon Sep 17 00:00:00 2001 From: Gary Date: Thu, 8 Oct 2026 20:20:57 +0200 Subject: [PATCH 2/2] fix(packages): the review's findings on the Docker line and the update dialogs The Docker line no longer says 'up to date' of a snap or of a Docker whose source apt cannot see, shows the snap's command, warns of a restart only when the pending packages restart Docker, and says 'no other updates' next to it; the confirmation names Docker only when the core reported it; a refusal of the ReCasaOS update (409) gives its button back with the reason instead of spinning for ever; a refused package update shows the reason once and checks the list again; the lint passes (a lookup table instead of a switch); the command is reachable by keyboard; French wording. --- src/assets/lang/en_US.json | 8 +- src/assets/lang/fr_FR.json | 16 ++-- .../settings/SystemPackageUpdateModal.spec.js | 82 +++++++++++++++++-- .../settings/SystemPackageUpdateModal.vue | 53 +++++++----- src/components/settings/UpdateModal.spec.js | 31 +++++++ src/components/settings/UpdateModal.vue | 14 +++- 6 files changed, 167 insertions(+), 37 deletions(-) diff --git a/src/assets/lang/en_US.json b/src/assets/lang/en_US.json index 01282fc2..41063ced 100644 --- a/src/assets/lang/en_US.json +++ b/src/assets/lang/en_US.json @@ -88,7 +88,6 @@ "Checking for system package updates...": "Checking for system package updates...", "Applying system package updates...": "Applying system package updates...", "Finishing system package update...": "Finishing system package update...", - "Are you sure you want to update all available system packages?": "Are you sure you want to update all available system packages?", "The package list may change before the update starts.": "The package list may change before the update starts.", "Retry": "Retry", "Let more friends know": "Let more friends know", @@ -940,5 +939,10 @@ "Installed from your distribution's docker.io package.": "Installed from your distribution's docker.io package.", "Installed as a snap.": "Installed as a snap.", "Installed from a source ReCasaOS does not recognise.": "Installed from a source ReCasaOS does not recognise.", - "Are you sure you want to update the system packages listed? Docker is not part of this update.": "Are you sure you want to update the system packages listed? Docker is not part of this update." + "Are you sure you want to update the system packages listed? Docker is not part of this update.": "Are you sure you want to update the system packages listed? Docker is not part of this update.", + "No other system package updates are available.": "No other system package updates are available.", + "Command to update Docker": "Command to update Docker", + "ReCasaOS cannot see updates of a snap. Update it with the command below.": "ReCasaOS cannot see updates of a snap. Update it with the command below.", + "This machine's package sources offer no update for it, and ReCasaOS cannot tell more.": "This machine's package sources offer no update for it, and ReCasaOS cannot tell more.", + "Are you sure you want to update the system packages listed?": "Are you sure you want to update the system packages listed?" } diff --git a/src/assets/lang/fr_FR.json b/src/assets/lang/fr_FR.json index 2a54ad05..301e6f79 100644 --- a/src/assets/lang/fr_FR.json +++ b/src/assets/lang/fr_FR.json @@ -439,7 +439,6 @@ "Checking for system package updates...": "Recherche de mises à jour des paquets système...", "Applying system package updates...": "Application des mises à jour des paquets système...", "Finishing system package update...": "Finalisation de la mise à jour des paquets système...", - "Are you sure you want to update all available system packages?": "Voulez-vous vraiment mettre à jour tous les paquets système disponibles ?", "The package list may change before the update starts.": "La liste des paquets peut changer avant le début de la mise à jour.", "Retry": "Réessayer", "Merged Storage": "Stockage fusionné", @@ -930,15 +929,20 @@ "The update could not be started.": "La mise à jour n’a pas pu démarrer.", "The progress of the update could not be loaded.": "L’avancement de la mise à jour n’a pas pu être chargé.", "Docker": "Docker", - "Docker has updates. This update leaves it alone.": "Docker a des mises à jour. Cette mise à jour n'y touche pas.", - "Updating Docker restarts it: every container stops until it is back. Apps set to restart start again by themselves, the others stay stopped. Do it when that suits you.": "Mettre Docker à jour le redémarre : tous les conteneurs s'arrêtent jusqu'à son retour. Les apps configurées pour redémarrer repartent d'elles-mêmes, les autres restent arrêtées. Faites-le quand cela vous arrange.", + "Docker has updates. This update leaves it alone.": "Des mises à jour de Docker sont disponibles. Cette mise à jour ne les installe pas.", + "Updating Docker restarts it: every container stops until it is back. Apps set to restart start again by themselves, the others stay stopped. Do it when that suits you.": "La mise à jour de Docker le redémarre : tous les conteneurs s'arrêtent jusqu'à son retour. Les apps configurées pour redémarrer repartent d'elles-mêmes, les autres restent arrêtées. Faites-le quand cela vous arrange.", "In a terminal on this machine:": "Dans un terminal sur cette machine :", "ReCasaOS does not know how Docker was installed here, so it shows no command.": "ReCasaOS ne sait pas comment Docker a été installé ici, il n'affiche donc pas de commande.", "Docker is up to date.": "Docker est à jour.", - "Docker is not installed from a package here.": "Docker n'est pas installé depuis un paquet ici.", + "Docker is not installed from a package here.": "Docker n'a pas été installé à partir d'un paquet sur cette machine.", "Installed from Docker's own repository.": "Installé depuis le dépôt officiel de Docker.", "Installed from your distribution's docker.io package.": "Installé depuis le paquet docker.io de votre distribution.", - "Installed as a snap.": "Installé en snap.", + "Installed as a snap.": "Installé via snap.", "Installed from a source ReCasaOS does not recognise.": "Installé depuis une source que ReCasaOS ne reconnaît pas.", - "Are you sure you want to update the system packages listed? Docker is not part of this update.": "Voulez-vous vraiment mettre à jour les paquets système listés ? Docker ne fait pas partie de cette mise à jour." + "Are you sure you want to update the system packages listed? Docker is not part of this update.": "Voulez-vous vraiment mettre à jour les paquets système listés ? Docker ne fait pas partie de cette mise à jour.", + "No other system package updates are available.": "Aucune autre mise à jour de paquets système n'est disponible.", + "Command to update Docker": "Commande pour mettre Docker à jour", + "ReCasaOS cannot see updates of a snap. Update it with the command below.": "ReCasaOS ne voit pas les mises à jour d'un snap. Mettez-le à jour avec la commande ci-dessous.", + "This machine's package sources offer no update for it, and ReCasaOS cannot tell more.": "Les sources de paquets de cette machine n'offrent pas de mise à jour pour lui, et ReCasaOS ne peut pas en dire plus.", + "Are you sure you want to update the system packages listed?": "Voulez-vous vraiment mettre à jour les paquets système listés ?" } diff --git a/src/components/settings/SystemPackageUpdateModal.spec.js b/src/components/settings/SystemPackageUpdateModal.spec.js index 10f3c6bc..e25db217 100644 --- a/src/components/settings/SystemPackageUpdateModal.spec.js +++ b/src/components/settings/SystemPackageUpdateModal.spec.js @@ -63,6 +63,7 @@ describe('docker on a line of its own', () => { installed: true, origin: 'docker-repository', version: '29.8.1', + restarts_docker: true, updates: [{ name: 'docker-ce', current_version: '5:29.8.1-1', candidate_version: '5:29.8.2-1' }], manual_command: 'sudo apt-get update && sudo apt-get install --only-upgrade docker-ce docker-ce-cli containerd.io', } @@ -86,26 +87,60 @@ describe('docker on a line of its own', () => { expect(line.text()).toContain('docker-ce') expect(line.text()).toContain('every container stops until it is back') expect(line.find('pre.docker-command').text()).toBe(docker.manual_command) - // and Docker's package is not among the ones the button installs expect(w.find('.package-list').text()).toContain('libc6') - expect(w.find('.package-list').text()).not.toContain('docker-ce') }) - it('says it is up to date, with no command, when nothing is on offer', async () => { - const w = await open({ getSystemPackages: check({ docker: { ...docker, updates: [] } }) }) + it('says it is up to date, with no command, when apt has nothing on offer for a source it can see', async () => { + for (const origin of ['docker-repository', 'distribution']) { + const w = await open({ getSystemPackages: check({ docker: { ...docker, origin, updates: [] } }) }) - expect(w.find('.docker-line').text()).toContain('Docker is up to date.') + expect(w.find('.docker-line').text()).toContain('Docker is up to date.') + expect(w.find('.docker-line pre').exists()).toBe(false) + w.unmount() + } + }) + + it('does not say "up to date" of a snap, which apt cannot see, and gives the command', async () => { + const w = await open({ getSystemPackages: check({ docker: { installed: true, origin: 'snap', updates: [], manual_command: 'sudo snap refresh docker' } }) }) + + const line = w.find('.docker-line').text() + expect(line).toContain('Installed as a snap.') + expect(line).toContain('ReCasaOS cannot see updates of a snap. Update it with the command below.') + expect(line).not.toContain('Docker is up to date.') + expect(w.find('pre.docker-command').text()).toBe('sudo snap refresh docker') + }) + + it('does not say "up to date" of a Docker whose source it does not know, and shows no command', async () => { + const w = await open({ getSystemPackages: check({ docker: { installed: true, origin: 'unknown', version: '24.0.5', updates: [], manual_command: '' } }) }) + + const line = w.find('.docker-line').text() + expect(line).toContain('Installed from a source ReCasaOS does not recognise.') + expect(line).toContain('offer no update for it') + expect(line).not.toContain('Docker is up to date.') expect(w.find('.docker-line pre').exists()).toBe(false) }) - it('shows no command for a Docker whose origin it does not know, and says so', async () => { + it('says it has no command when it has updates for a source it does not know', async () => { const w = await open({ getSystemPackages: check({ docker: { ...docker, origin: 'unknown', manual_command: '' } }) }) - expect(w.find('.docker-line').text()).toContain('Installed from a source ReCasaOS does not recognise.') expect(w.find('.docker-line').text()).toContain('ReCasaOS does not know how Docker was installed here, so it shows no command.') expect(w.find('.docker-line pre').exists()).toBe(false) }) + it('warns that Docker restarts only when what is pending restarts it', async () => { + const plugin = { ...docker, restarts_docker: false, updates: [{ name: 'docker-compose-plugin', current_version: '2.40.0', candidate_version: '2.40.1' }] } + const w = await open({ getSystemPackages: check({ docker: plugin }) }) + + expect(w.find('.docker-line').text()).toContain('Docker has updates.') + expect(w.find('.docker-line').text()).not.toContain('every container stops until it is back') + }) + + it('copes with a Docker object whose updates are null', async () => { + const w = await open({ getSystemPackages: check({ docker: { ...docker, updates: null } }) }) + + expect(w.find('.docker-line').text()).toContain('Docker is up to date.') + }) + it('has no Docker line when the host reports none', async () => { const w = await open({ getSystemPackages: check({}) }) @@ -115,11 +150,18 @@ describe('docker on a line of its own', () => { it('shows Docker even when nothing else is to update, next to the "no updates" sentence', async () => { const w = await open({ getSystemPackages: check({ updates: [], count: 0, docker }) }) - expect(w.text()).toContain('No system package updates are available.') + expect(w.text()).toContain('No other system package updates are available.') expect(w.find('.docker-line').text()).toContain('docker-ce') expect(w.text()).not.toContain('Update packages') }) + it('says plainly that nothing is available when Docker has nothing either', async () => { + const w = await open({ getSystemPackages: check({ updates: [], count: 0, docker: { ...docker, updates: [] } }) }) + + expect(w.text()).toContain('No system package updates are available.') + expect(w.text()).not.toContain('No other') + }) + it('asks for confirmation of the listed packages and says Docker is not part of it', async () => { const confirm = vi.fn() const w = await open({ getSystemPackages: check({ docker }) }, { $buefy: { dialog: { confirm } } }) @@ -130,4 +172,28 @@ describe('docker on a line of its own', () => { expect(confirm.mock.calls[0][0].message).toContain('Docker is not part of this update.') expect(confirm.mock.calls[0][0].message).not.toContain('all available') }) + + it('does not say Docker is not part of the update when the core did not say so', async () => { + // an older core lists Docker's packages with the rest and installs them: the sentence would be false + const confirm = vi.fn() + const w = await open({ getSystemPackages: check({}) }, { $buefy: { dialog: { confirm } } }) + + w.vm.confirmUpdate() + + expect(confirm.mock.calls[0][0].message).not.toContain('Docker') + }) + + it('shows the core\'s reason once when the update is refused, and checks the list again', async () => { + const reason = 'another update or package operation is running on this box: casaos-update is running' + const refusal = Object.assign(new Error('Request failed with status code 409'), { response: { status: 409, data: { message: reason, data: { ...idle, error: reason } } } }) + const getSystemPackages = vi.fn(check({ docker })) + const w = await open({ getSystemPackages, startSystemPackageUpdate: () => Promise.reject(refusal) }) + expect(getSystemPackages).toHaveBeenCalledTimes(1) + + await w.vm.startUpdate() + await flushPromises() + + expect(w.text().split(reason).length - 1).toBe(1) + expect(getSystemPackages).toHaveBeenCalledTimes(2) + }) }) diff --git a/src/components/settings/SystemPackageUpdateModal.vue b/src/components/settings/SystemPackageUpdateModal.vue index 353af2ff..48b95520 100644 --- a/src/components/settings/SystemPackageUpdateModal.vue +++ b/src/components/settings/SystemPackageUpdateModal.vue @@ -29,7 +29,7 @@

- {{ $t('No system package updates are available.') }} + {{ $t(dockerHasUpdates ? 'No other system package updates are available.' : 'No system package updates are available.') }}
@@ -58,7 +58,8 @@

- {{ $t('Docker') }}{{ info.docker.version }} + {{ $t('Docker') }} + {{ info.docker.version }}

{{ dockerOrigin }}

-

{{ $t('Docker is up to date.') }}

+

{{ $t('Docker is up to date.') }}

+

{{ dockerUnseen }}

+ +

{{ $t('ReCasaOS does not know how Docker was installed here, so it shows no command.') }}

@@ -91,7 +93,7 @@
{{ $t('System package update failed.') }}
-

+

{{ status.error }}

@@ -165,21 +167,28 @@ export default { dockerHasUpdates() { return Array.isArray(this.info.docker?.updates) && this.info.docker.updates.length > 0 }, + // apt tells of an update for Docker's own repository and for the distribution's package; + // of a snap, or of a Docker whose source it does not know, it tells nothing, and "up to + // date" would be a claim nobody checked + dockerCanSeeUpdates() { + return ['docker-repository', 'distribution'].includes(this.info.docker?.origin) + }, dockerOrigin() { const docker = this.info.docker if (!docker?.installed) { return this.$t('Docker is not installed from a package here.') } - switch (docker.origin) { - case 'docker-repository': - return this.$t('Installed from Docker\'s own repository.') - case 'distribution': - return this.$t('Installed from your distribution\'s docker.io package.') - case 'snap': - return this.$t('Installed as a snap.') - default: - return this.$t('Installed from a source ReCasaOS does not recognise.') + const words = { + 'docker-repository': 'Installed from Docker\'s own repository.', + 'distribution': 'Installed from your distribution\'s docker.io package.', + 'snap': 'Installed as a snap.', } + return this.$t(words[docker.origin] || 'Installed from a source ReCasaOS does not recognise.') + }, + dockerUnseen() { + return this.$t(this.info.docker?.origin === 'snap' + ? 'ReCasaOS cannot see updates of a snap. Update it with the command below.' + : 'This machine\'s package sources offer no update for it, and ReCasaOS cannot tell more.') }, }, mounted() { @@ -225,7 +234,7 @@ export default { confirmUpdate() { this.$buefy.dialog.confirm({ title: this.$t('Update system packages'), - message: `${this.$t('Are you sure you want to update the system packages listed? Docker is not part of this update.')}

${this.$t('The package list may change before the update starts.')}`, + message: `${this.$t(this.info.docker ? 'Are you sure you want to update the system packages listed? Docker is not part of this update.' : 'Are you sure you want to update the system packages listed?')}

${this.$t('The package list may change before the update starts.')}`, type: 'is-warning', hasIcon: true, confirmText: this.$t('Update packages'), @@ -248,6 +257,10 @@ export default { } } this.fail('Could not start the system package update.', error) + // refused (409) for a reason other than "already running": what was on screen may no longer be so + if (error?.response?.status === 409 && this.status.state !== 'running' && !this.isReconciliationPending) { + this.checkPackages(true) + } } finally { this.isStarting = false } diff --git a/src/components/settings/UpdateModal.spec.js b/src/components/settings/UpdateModal.spec.js index 1446e6d6..29fe3567 100644 --- a/src/components/settings/UpdateModal.spec.js +++ b/src/components/settings/UpdateModal.spec.js @@ -109,3 +109,34 @@ describe('the update dialog opened on the log', () => { wrapper.unmount() }) }) + +// A refusal is a normal answer now: the core says 409 while a package update or a package +// manager is working. The button used to spin for ever. +describe('a refused update', () => { + it('gives the button back, says why, and does not follow a log that is not there', async () => { + const getContent = vi.fn(() => Promise.resolve({ data: { data: '' } })) + const toast = vi.fn() + const refusal = Object.assign(new Error('Request failed with status code 409'), { + response: { status: 409, data: { message: 'another update or package operation is running on this box: casaos-package-update is running' } }, + }) + const wrapper = mount(UpdateModal, { + global: { + mocks: { + $t: k => k, + $api: { file: { getContent }, sys: { updateCasaOS: vi.fn(() => Promise.reject(refusal)) } }, + $buefy: { toast: { open: toast } }, + }, + stubs: { 'b-button': true, 'b-loading': true }, + }, + }) + + await wrapper.vm.updateSystem() + + expect(wrapper.vm.isUpdating).toBe(false) + expect(toast).toHaveBeenCalledTimes(1) + expect(toast.mock.calls[0][0].message).toContain('The update could not be started.') + expect(toast.mock.calls[0][0].message).toContain('casaos-package-update is running') + expect(getContent).not.toHaveBeenCalled() + wrapper.unmount() + }) +}) diff --git a/src/components/settings/UpdateModal.vue b/src/components/settings/UpdateModal.vue index 3138993d..db6f1994 100644 --- a/src/components/settings/UpdateModal.vue +++ b/src/components/settings/UpdateModal.vue @@ -97,7 +97,19 @@ export default { */ async updateSystem() { this.isUpdating = true - await this.$api.sys.updateCasaOS() + try { + await this.$api.sys.updateCasaOS() + } catch (error) { + // refused (409: a package update or a package manager is working) or not started: + // the button comes back, with the core's reason when it gave one + this.isUpdating = false + const detail = error?.response?.data?.message + this.$buefy.toast.open({ + message: this.$t('The update could not be started.') + (typeof detail === 'string' && detail ? ` ${detail}` : ''), + type: 'is-danger', + }) + return + } // this.checkUpdateState(); this.getUpdateLogs() },