From 9c358d632f357e7f561c362622b2d58413e44ca7 Mon Sep 17 00:00:00 2001 From: Stephane de Labrusse Date: Wed, 29 Jul 2026 14:34:05 +0200 Subject: [PATCH 1/2] fix(ui): count ACME and certificate reads upfront The counters were incremented inside the loop and decremented through a helper clamped with Math.max(0, n - 1). The clamp hides two things: the counter reaches zero between two iterations, so the table can leave its loading state while the next task is still being created, and a surplus decrement is silently swallowed instead of being impossible. Set the counter to the number of instances before the loop and decrement it plainly. clearTaskListeners() already makes a stale event unable to reach a handler, so nothing is left to clamp. Release listCertificatesNum when list-installed-modules is aborted: the certificates chain never runs, and the skeleton rows stayed for good when the abort followed a reconnection. --- .../src/components/settings/AcmeSettings.vue | 18 ++++++----------- .../settings/SettingsTlsCertificates.vue | 20 ++++++++----------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/core/ui/src/components/settings/AcmeSettings.vue b/core/ui/src/components/settings/AcmeSettings.vue index 4be8e48f2..aa31ebee8 100644 --- a/core/ui/src/components/settings/AcmeSettings.vue +++ b/core/ui/src/components/settings/AcmeSettings.vue @@ -218,15 +218,16 @@ export default { async getAcmeServer() { // a handler of a previous round would decrement the counter of this one this.clearTaskListeners(); - this.loading.getAcmeServerNum = 0; this.servers = []; // otherwise a transient error sticks after the nodes answer again this.clearTableError(); + // count the whole batch upfront: the counter must not reach zero between + // two iterations + this.loading.getAcmeServerNum = this.traefikInstances.length; for (const traefikInstance of this.traefikInstances) { const taskAction = "get-acme-server"; const eventId = this.getUuid(); - this.loading.getAcmeServerNum++; // register to task events @@ -262,7 +263,7 @@ export default { this.error.getAcmeServer = errMessage; this.currentErrorAction = this.$t("action." + taskAction); this.currentErrorDescription = errMessage; - this.decreaseGetAcmeServerNum(); + this.loading.getAcmeServerNum--; } } }, @@ -271,7 +272,7 @@ export default { this.error.getAcmeServer = this.$t("error.generic_error"); this.currentErrorAction = this.$t("action." + taskContext.action); this.currentErrorDescription = this.$t("error.generic_error"); - this.decreaseGetAcmeServerNum(); + this.loading.getAcmeServerNum--; }, getAcmeServerCompleted(taskContext, taskResult) { const server = taskResult.output; @@ -297,14 +298,7 @@ export default { this.servers.push(server); } this.servers.sort(this.sortByProperty("node")); - this.decreaseGetAcmeServerNum(); - }, - decreaseGetAcmeServerNum() { - // never go below zero: a new round resets the counter - this.loading.getAcmeServerNum = Math.max( - 0, - this.loading.getAcmeServerNum - 1 - ); + this.loading.getAcmeServerNum--; }, onReloadServers() { this.getAcmeServer(); diff --git a/core/ui/src/views/settings/SettingsTlsCertificates.vue b/core/ui/src/views/settings/SettingsTlsCertificates.vue index ad46ea072..262488685 100644 --- a/core/ui/src/views/settings/SettingsTlsCertificates.vue +++ b/core/ui/src/views/settings/SettingsTlsCertificates.vue @@ -980,6 +980,8 @@ export default { this.currentErrorAction = this.$t("action." + taskContext.action); this.currentErrorDescription = this.$t("error.generic_error"); this.loading.listInstalledModules = false; + // the certificates chain will not run: release its skeleton rows + this.loading.listCertificatesNum = 0; }, listInstalledModulesCompleted(taskContext, taskResult) { // init nodes @@ -1038,12 +1040,13 @@ export default { this.clearTaskListeners(); this.offlineTraefikInstances = []; this.listCertificatesErrors = []; - this.loading.listCertificatesNum = 0; + // count the whole batch upfront: the counter must not reach zero between + // two iterations + this.loading.listCertificatesNum = this.traefikInstances.length; for (const traefikInstance of this.traefikInstances) { const taskAction = "list-certificates"; const eventId = this.getUuid(); - this.loading.listCertificatesNum++; // register to task events @@ -1109,7 +1112,7 @@ export default { )} - ${this.getNodeLabel(traefikInstance)})`, }); } - this.decreaseListCertificatesNum(); + this.loading.listCertificatesNum--; } } }, @@ -1128,7 +1131,7 @@ export default { traefikInstance )} - ${this.getNodeLabel(traefikInstance)})`, }); - this.decreaseListCertificatesNum(); + this.loading.listCertificatesNum--; }, listCertificatesCompleted(taskContext, taskResult) { const traefikId = taskContext.extra.traefikInstance.id; @@ -1203,14 +1206,7 @@ export default { // $set() is needed for reactivity (see https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects) this.$set(this.certificatesByTraefikId, traefikId, certs); - this.decreaseListCertificatesNum(); - }, - decreaseListCertificatesNum() { - // never go below zero: a new round resets the counter - this.loading.listCertificatesNum = Math.max( - 0, - this.loading.listCertificatesNum - 1 - ); + this.loading.listCertificatesNum--; }, getTagKind(status) { switch (status) { From 631a161eab5e3e56b1c4d89bebcd81bf938721ad Mon Sep 17 00:00:00 2001 From: Stephane de Labrusse Date: Wed, 29 Jul 2026 14:34:05 +0200 Subject: [PATCH 2/2] fix(ui): keep the ACME rows readable when one node fails error.getAcmeServer is a single shared string and NsDataTable renders its error notification instead of the rows, so one unreachable node hid the settings of every node that did answer. Show the notification above the table when there are rows to keep, and leave isErrorShown to the case where there is nothing else to display. --- core/ui/src/components/settings/AcmeSettings.vue | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/ui/src/components/settings/AcmeSettings.vue b/core/ui/src/components/settings/AcmeSettings.vue index aa31ebee8..a7b9168b0 100644 --- a/core/ui/src/components/settings/AcmeSettings.vue +++ b/core/ui/src/components/settings/AcmeSettings.vue @@ -8,6 +8,15 @@ + + 0; }, + tableError() { + return this.instancesError || this.error.getAcmeServer; + }, tableErrorTitle() { return this.instancesError ? this.$t("action.list-installed-modules")