diff --git a/core/ui/public/i18n/en/translation.json b/core/ui/public/i18n/en/translation.json index d0afa9188..f1944b198 100644 --- a/core/ui/public/i18n/en/translation.json +++ b/core/ui/public/i18n/en/translation.json @@ -913,7 +913,9 @@ "invalid_format": "Invalid format", "cert_verification_failed_selfsigned": "Self-signed certificates are not allowed", "cert_verification_failed_chain": "Certificate chain is incomplete or invalid", - "newcert_acme_error": "Cannot obtain certificate. Check the 'Request certificate' error in the notification drawer for details." + "newcert_acme_error": "Cannot obtain certificate. Check the 'Request certificate' error in the notification drawer for details.", + "node_is_offline": "{node} is offline", + "certificates_not_displayed": "Certificates configured on {instanceId} are not displayed" }, "settings_acme_servers": { "title": "ACME servers", diff --git a/core/ui/src/views/settings/SettingsTlsCertificates.vue b/core/ui/src/views/settings/SettingsTlsCertificates.vue index 8abb83956..d61cce9bd 100644 --- a/core/ui/src/views/settings/SettingsTlsCertificates.vue +++ b/core/ui/src/views/settings/SettingsTlsCertificates.vue @@ -53,6 +53,30 @@ +
+ +
+
+ +
{ + this.listCertificatesAborted( + taskResult, + taskContext, + traefikInstance + ); + } ); this.$root.$once( @@ -930,20 +964,54 @@ export default { const err = res[0]; if (err) { - console.error(`error creating task ${taskAction}`, err); - const errMessage = this.getErrorMessage(err); - this.error.listCertificates = errMessage; - this.currentErrorAction = this.$t("action." + taskAction); - this.currentErrorDescription = errMessage; + console.error( + `error creating task ${taskAction} for instance ${traefikInstance.id} on node ${traefikInstance.node}`, + err + ); + // Clean up orphaned event listeners since the task was never created + this.$root.$off(`${taskAction}-aborted-${eventId}`); + this.$root.$off(`${taskAction}-completed-${eventId}`); + + if (err.response && err.response.status === 404) { + // 404 means the module API is unreachable: node is offline + if ( + traefikInstance && + !this.offlineTraefikInstances.find( + (instance) => instance.id === traefikInstance.id + ) + ) { + this.offlineTraefikInstances.push(traefikInstance); + } + } else { + // Unexpected error: display a generic inline error notification + this.listCertificatesErrors.push({ + title: this.$t("action." + taskAction), + description: `${this.$t( + "error.generic_error" + )} (${this.getTraefikInstanceLabel( + traefikInstance + )} - ${this.getNodeLabel(traefikInstance)})`, + }); + } this.loading.listCertificatesNum--; } } }, - listCertificatesAborted(taskResult, taskContext) { - console.error(`${taskContext.action} aborted`, taskResult); - this.error.listCertificates = this.$t("error.generic_error"); - this.currentErrorAction = this.$t("action." + taskContext.action); - this.currentErrorDescription = this.$t("error.generic_error"); + listCertificatesAborted(taskResult, taskContext, traefikInstance) { + console.error( + `${taskContext.action} aborted for instance ${traefikInstance.id} on node ${traefikInstance.node}`, + taskResult + ); + + // Add error to array + this.listCertificatesErrors.push({ + title: this.$t("action." + taskContext.action), + description: `${this.$t( + "error.generic_error" + )} (${this.getTraefikInstanceLabel( + traefikInstance + )} - ${this.getNodeLabel(traefikInstance)})`, + }); this.loading.listCertificatesNum--; }, listCertificatesCompleted(taskContext, taskResult) { @@ -1032,6 +1100,29 @@ export default { return "gray"; } }, + getNodeLabel(instance) { + const nodeLabel = instance.node_ui_name + ? instance.node_ui_name + + " (" + + this.$t("common.node") + + " " + + instance.node + + ")" + : this.$t("common.node") + " " + instance.node; + return nodeLabel; + }, + getTraefikInstanceLabel(instance) { + let label = instance.id; + if (instance.ui_name && instance.ui_name.trim()) { + label = `${instance.ui_name}(${instance.id})`; + } + return label; + }, + getOfflineInstanceDescription(instance) { + return this.$t("settings_tls_certificates.certificates_not_displayed", { + instanceId: this.getTraefikInstanceLabel(instance), + }); + }, clearFilters() { this.q.selectedNodeId = "any"; this.filter.text = "";