From e78a63c67f0ddfda3833633eecaa6f552055d692 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 7 Jul 2025 13:35:54 +0200 Subject: [PATCH 01/25] add link to support form on error message --- src/servala/core/models/service.py | 72 +++++++++++++++++++-------- src/servala/frontend/views/mixins.py | 24 +++++++++ src/servala/frontend/views/service.py | 23 ++++++--- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 362661b..ebc2878 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -10,6 +10,7 @@ from django.core.exceptions import ValidationError from django.db import IntegrityError, models, transaction from django.utils import timezone from django.utils.functional import cached_property +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from encrypted_fields.fields import EncryptedJSONField from kubernetes import client, config @@ -19,6 +20,10 @@ from servala.core import rules as perms from servala.core.models.mixins import ServalaModelMixin from servala.core.validators import kubernetes_name_validator +SUPPORT_MESSAGE_TEMPLATE = _( + "Need help? We're happy to help via the support form." +) + class ServiceCategory(ServalaModelMixin, models.Model): """ @@ -615,10 +620,9 @@ class ServiceInstance(ServalaModelMixin, models.Model): context=context, ) except IntegrityError: - raise ValidationError( - _( - "An instance with this name already exists in this organization. Please choose a different name." - ) + raise cls._format_error_with_support( + "An instance with this name already exists in this organization. Please choose a different name.", + organization, ) try: @@ -657,10 +661,16 @@ class ServiceInstance(ServalaModelMixin, models.Model): try: error_body = json.loads(e.body) reason = error_body.get("message", str(e)) - raise ValidationError(_("Kubernetes API error: {}").format(reason)) + raise cls._format_error_with_support( + "Kubernetes API error: {error}.", organization, error=reason + ) except (ValueError, TypeError): - raise ValidationError(_("Kubernetes API error: {}").format(str(e))) - raise ValidationError(_("Error creating instance: {}").format(str(e))) + raise cls._format_error_with_support( + "Kubernetes API error: {error}.", organization, error=str(e) + ) + raise cls._format_error_with_support( + "Error creating instance: {error}.", organization, error=str(e) + ) return instance def update_spec(self, spec_data, updated_by): @@ -681,28 +691,27 @@ class ServiceInstance(ServalaModelMixin, models.Model): self.save() # Updates updated_at timestamp except ApiException as e: if e.status == 404: - raise ValidationError( - _( - "Service instance not found in Kubernetes. It may have been deleted externally." - ) + raise self._format_error_with_support( + "Service instance not found in Kubernetes. It may have been deleted externally.", + self.organization, ) try: error_body = json.loads(e.body) reason = error_body.get("message", str(e)) - raise ValidationError( - _("Kubernetes API error updating instance: {error}").format( - error=reason - ) + raise self._format_error_with_support( + "Kubernetes API error updating instance: {error}.", + self.organization, + error=reason, ) except (ValueError, TypeError): - raise ValidationError( - _("Kubernetes API error updating instance: {error}").format( - error=str(e) - ) + raise self._format_error_with_support( + "Kubernetes API error updating instance: {error}.", + self.organization, + error=str(e), ) except Exception as e: - raise ValidationError( - _("Error updating instance: {error}").format(error=str(e)) + raise self._format_error_with_support( + "Error updating instance: {error}.", self.organization, error=str(e) ) @transaction.atomic @@ -854,3 +863,24 @@ class ServiceInstance(ServalaModelMixin, models.Model): return {"error": str(e)} except Exception as e: return {"error": str(e)} + + @classmethod + def _format_error_with_support(cls, message, organization, **kwargs): + """ + Helper method to format error messages with support links. + + Args: + message: The error message template (without support text) + organization: The organization object to get the support URL + **kwargs: Additional format parameters for the message + + Returns: + A ValidationError with the formatted message including support link + """ + support_url = organization.urls.support + # Combine the main message with the support message template + full_message = _("{message} {support_message}").format( + message=_(message).format(**kwargs), + support_message=SUPPORT_MESSAGE_TEMPLATE.format(support_url=support_url), + ) + return ValidationError(mark_safe(full_message)) diff --git a/src/servala/frontend/views/mixins.py b/src/servala/frontend/views/mixins.py index fd5a494..7e7b453 100644 --- a/src/servala/frontend/views/mixins.py +++ b/src/servala/frontend/views/mixins.py @@ -1,9 +1,14 @@ from django.utils.functional import cached_property +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from django.views.generic import UpdateView from rules.contrib.views import AutoPermissionRequiredMixin, PermissionRequiredMixin from servala.core.models import Organization +# Import the support message template from the service model +from servala.core.models.service import SUPPORT_MESSAGE_TEMPLATE + class HtmxViewMixin: fragments = [] @@ -90,3 +95,22 @@ class OrganizationViewMixin(PermissionRequiredMixin): def has_permission(self): return self.has_organization_permission() and super().has_permission() + + def format_error_with_support(self, message, **kwargs): + """ + Helper method to format error messages with support links for frontend views. + + Args: + message: The error message template (without support text) + **kwargs: Additional format parameters for the message + + Returns: + A formatted message with support link + """ + support_url = self.organization.urls.support + # Combine the main message with the support message template + full_message = _("{message} {support_message}").format( + message=_(message).format(**kwargs), + support_message=SUPPORT_MESSAGE_TEMPLATE.format(support_url=support_url), + ) + return mark_safe(full_message) diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 2d74842..49ac683 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -144,7 +144,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView form = self.get_instance_form() if not form: # Should not happen if context_object is valid, but as a safeguard - messages.error(self.request, _("Could not initialize service form.")) + messages.error( + self.request, + self.format_error_with_support("Could not initialize service form."), + ) return self.render_to_response(context) if form.is_valid(): @@ -161,7 +164,10 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView messages.error(self.request, e.message or str(e)) except Exception as e: messages.error( - self.request, _("Error creating instance: {}").format(str(e)) + self.request, + self.format_error_with_support( + "Error creating instance: {error}.", error=str(e) + ), ) # If the form is not valid or if the service creation failed, we render it again @@ -366,7 +372,10 @@ class ServiceInstanceUpdateView( return self.form_invalid(form) except Exception as e: messages.error( - self.request, _("Error updating instance: {error}").format(error=str(e)) + self.request, + self.format_error_with_support( + "Error updating instance: {error}.", error=str(e) + ), ) return self.form_invalid(form) @@ -435,9 +444,11 @@ class ServiceInstanceDeleteView( except Exception as e: messages.error( self.request, - _( - "An error occurred while trying to delete instance '{name}': {error}" - ).format(name=self.object.name, error=str(e)), + self.format_error_with_support( + "An error occurred while trying to delete instance '{name}': {error}.", + name=self.object.name, + error=str(e), + ), ) response = HttpResponse() response["HX-Redirect"] = str(self.object.urls.base) From 5fa3d32c57549b11c48bc285a09616c59150a0ce Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 7 Jul 2025 13:38:09 +0200 Subject: [PATCH 02/25] do not dismiss message when error message --- src/servala/frontend/templates/includes/message.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/frontend/templates/includes/message.html b/src/servala/frontend/templates/includes/message.html index 59260d2..d5debc9 100644 --- a/src/servala/frontend/templates/includes/message.html +++ b/src/servala/frontend/templates/includes/message.html @@ -9,7 +9,7 @@ - {% block html_title %}Dashboard{% endblock html_title %} – Servala + + {% block html_title %} + Dashboard + {% endblock html_title %} + – Servala diff --git a/src/servala/frontend/templates/frontend/profile.html b/src/servala/frontend/templates/frontend/profile.html index dc12100..b9c1799 100644 --- a/src/servala/frontend/templates/frontend/profile.html +++ b/src/servala/frontend/templates/frontend/profile.html @@ -116,7 +116,8 @@ {% endblocktranslate %}

- {% translate "VSHN Account Console" %} diff --git a/src/servala/frontend/templates/includes/message.html b/src/servala/frontend/templates/includes/message.html index 59260d2..3125da1 100644 --- a/src/servala/frontend/templates/includes/message.html +++ b/src/servala/frontend/templates/includes/message.html @@ -1,28 +1,29 @@ -
+
{{ message }}
- \ No newline at end of file + document.addEventListener('DOMContentLoaded', function() { + const alert = document.getElementById('auto-dismiss-alert-{{ forloop.counter0|default:' + 0 ' }}'); + if (alert) { + setTimeout(function() { + let opacity = 1; + const fadeOutInterval = setInterval(function() { + if (opacity > 0.05) { + opacity -= 0.05; + alert.style.opacity = opacity; + } else { + clearInterval(fadeOutInterval); + const bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + } + }, 25); + }, 5000); + } + }); + From 8a45a2275989f96ed29b2fab7dee462e0a724305 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 11 Jul 2025 14:43:26 +0200 Subject: [PATCH 13/25] sanitize kubernetes messages --- src/servala/core/models/service.py | 82 ++++++++++++++----- .../templates/includes/k8s_error.html | 14 ++++ .../frontend/templatetags/error_filters.py | 43 ++++++++++ 3 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 src/servala/frontend/templates/includes/k8s_error.html create mode 100644 src/servala/frontend/templatetags/error_filters.py diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index b4d27b3..1bdb678 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -1,4 +1,5 @@ import copy +import html import json import re @@ -607,29 +608,60 @@ class ServiceInstance(ServalaModelMixin, models.Model): @classmethod def _format_kubernetes_error(cls, error_message): - """ - Format Kubernetes API error messages for better user experience. - Converts validation error arrays into unordered lists. - """ - # Pattern to match validation errors in brackets + if not error_message: + return {"message": "", "errors": None, "has_list": False} + + error_message = str(error_message).strip() + + # Pattern to match validation errors in brackets like [error1, error2, error3] pattern = r"\[([^\]]+)\]" match = re.search(pattern, error_message) if not match: - return error_message + return {"message": error_message, "errors": None, "has_list": False} - errors_text = match.group(1) - # Split by comma and clean up each error - errors = [error.strip() for error in errors_text.split(",")] + errors_text = match.group(1).strip() - if len(errors) > 1: - # Format as HTML unordered list - error_list = "".join(f"
  • {error}
  • " for error in errors) - # Replace the bracketed section with the formatted list - formatted_message = re.sub(pattern, f"
      {error_list}
    ", error_message) - return mark_safe(formatted_message) + if "," not in errors_text: + return {"message": error_message, "errors": None, "has_list": False} - return error_message + errors = [error.strip().strip("\"'") for error in errors_text.split(",")] + errors = [error for error in errors if error] + + if len(errors) <= 1: + return {"message": error_message, "errors": None, "has_list": False} + + base_message = re.sub(pattern, "", error_message).strip() + base_message = base_message.rstrip(":").strip() + + return {"message": base_message, "errors": errors, "has_list": True} + + @classmethod + def _safe_format_error(cls, error_data): + if not isinstance(error_data, dict): + return html.escape(str(error_data)) + + if not error_data.get("has_list", False): + return html.escape(error_data.get("message", "")) + + message = html.escape(error_data.get("message", "")) + errors = error_data.get("errors", []) + + if not errors: + return message + + escaped_errors = [html.escape(str(error)) for error in errors] + error_items = "".join(f"
  • {error}
  • " for error in escaped_errors) + + if message: + return mark_safe(f"{message}
      {error_items}
    ") + else: + return mark_safe(f"
      {error_items}
    ") + + @classmethod + def format_error_for_display(cls, error_message): + error_data = cls._format_kubernetes_error(error_message) + return cls._safe_format_error(error_data) @classmethod def create_instance(cls, name, organization, context, created_by, spec_data): @@ -684,18 +716,21 @@ class ServiceInstance(ServalaModelMixin, models.Model): try: error_body = json.loads(e.body) reason = error_body.get("message", str(e)) - formatted_reason = cls._format_kubernetes_error(reason) + error_data = cls._format_kubernetes_error(reason) + formatted_reason = cls._safe_format_error(error_data) message = _("Error reported by control plane: {reason}").format( reason=formatted_reason ) raise ValidationError(organization.add_support_message(message)) except (ValueError, TypeError): - formatted_error = cls._format_kubernetes_error(str(e)) + error_data = cls._format_kubernetes_error(str(e)) + formatted_error = cls._safe_format_error(error_data) message = _("Error reported by control plane: {error}").format( error=formatted_error ) raise ValidationError(organization.add_support_message(message)) - formatted_error = cls._format_kubernetes_error(str(e)) + error_data = cls._format_kubernetes_error(str(e)) + formatted_error = cls._safe_format_error(error_data) message = _("Error creating instance: {error}").format( error=formatted_error ) @@ -727,19 +762,22 @@ class ServiceInstance(ServalaModelMixin, models.Model): try: error_body = json.loads(e.body) reason = error_body.get("message", str(e)) - formatted_reason = self._format_kubernetes_error(reason) + error_data = self._format_kubernetes_error(reason) + formatted_reason = self._safe_format_error(error_data) message = _( "Error reported by control plane while updating instance: {reason}" ).format(reason=formatted_reason) raise ValidationError(self.organization.add_support_message(message)) except (ValueError, TypeError): - formatted_error = self._format_kubernetes_error(str(e)) + error_data = self._format_kubernetes_error(str(e)) + formatted_error = self._safe_format_error(error_data) message = _( "Error reported by control plane while updating instance: {error}" ).format(error=formatted_error) raise ValidationError(self.organization.add_support_message(message)) except Exception as e: - formatted_error = self._format_kubernetes_error(str(e)) + error_data = self._format_kubernetes_error(str(e)) + formatted_error = self._safe_format_error(error_data) message = _("Error updating instance: {error}").format( error=formatted_error ) diff --git a/src/servala/frontend/templates/includes/k8s_error.html b/src/servala/frontend/templates/includes/k8s_error.html new file mode 100644 index 0000000..f97474d --- /dev/null +++ b/src/servala/frontend/templates/includes/k8s_error.html @@ -0,0 +1,14 @@ +{% if show_error %} +
    + {% if has_list %} + {% if message %}{{ message }}{% endif %} +
      + {% for error in errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% else %} + {{ message }} + {% endif %} +
    +{% endif %} diff --git a/src/servala/frontend/templatetags/error_filters.py b/src/servala/frontend/templatetags/error_filters.py new file mode 100644 index 0000000..18c4c4d --- /dev/null +++ b/src/servala/frontend/templatetags/error_filters.py @@ -0,0 +1,43 @@ +""" +Template filters for safe error formatting. +""" + +import html +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.filter +def format_k8s_error(error_data): + """ + Template filter to safely format Kubernetes error data. + Usage: {{ error_data|format_k8s_error }} + + Args: + error_data: Dictionary with structure from _format_kubernetes_error method + or a simple string + + Returns: + Safely formatted HTML string + """ + if not error_data: + return "" + + if not error_data.get("has_list", False): + return html.escape(error_data.get("message", "")) + + message = html.escape(error_data.get("message", "")) + errors = error_data.get("errors", []) + + if not errors: + return message + + escaped_errors = [html.escape(str(error)) for error in errors] + error_items = "".join(f"
  • {error}
  • " for error in escaped_errors) + + if message: + return mark_safe(f"{message}
      {error_items}
    ") + else: + return mark_safe(f"
      {error_items}
    ") From b5d691e407f99492ac8397a81aa8beedf480a996 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 11 Jul 2025 14:45:27 +0200 Subject: [PATCH 14/25] remove unused def --- src/servala/core/models/service.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 1bdb678..4c6ecd0 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -658,11 +658,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): else: return mark_safe(f"
      {error_items}
    ") - @classmethod - def format_error_for_display(cls, error_message): - error_data = cls._format_kubernetes_error(error_message) - return cls._safe_format_error(error_data) - @classmethod def create_instance(cls, name, organization, context, created_by, spec_data): # Ensure the namespace exists From 0bd895c4868925634e8d5b298df0b1564383ca7b Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Jul 2025 16:37:45 +0200 Subject: [PATCH 15/25] Make rules compatible with instance checks --- src/servala/core/rules.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/servala/core/rules.py b/src/servala/core/rules.py index 814a8ea..cf4dc1c 100644 --- a/src/servala/core/rules.py +++ b/src/servala/core/rules.py @@ -13,17 +13,30 @@ def has_organization_role(user, org, roles): @rules.predicate -def is_organization_owner(user, org): +def is_organization_owner(user, obj): + if hasattr(obj, "organization"): + org = obj.organization + else: + org = obj return has_organization_role(user, org, ["owner"]) @rules.predicate -def is_organization_admin(user, org): +def is_organization_admin(user, obj): + if hasattr(obj, "organization"): + org = obj.organization + else: + org = obj return has_organization_role(user, org, ["owner", "admin"]) @rules.predicate -def is_organization_member(user, org): +def is_organization_member(user, obj): + if hasattr(obj, "organization"): + org = obj.organization + else: + org = obj return has_organization_role(user, org, None) + rules.add_perm("core", rules.is_staff) From 6691b775f2d93292493c2df41c24772bd0e53eaf Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 14 Jul 2025 03:01:18 +0000 Subject: [PATCH 16/25] Lock file maintenance --- uv.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/uv.lock b/uv.lock index e5236dd..05cbe97 100644 --- a/uv.lock +++ b/uv.lock @@ -37,11 +37,11 @@ wheels = [ [[package]] name = "asgiref" -version = "3.9.0" +version = "3.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/68/fb4fb78c9eac59d5e819108a57664737f855c5a8e9b76aec1738bb137f9e/asgiref-3.9.0.tar.gz", hash = "sha256:3dd2556d0f08c4fab8a010d9ab05ef8c34565f6bf32381d17505f7ca5b273767", size = 36772, upload-time = "2025-07-03T13:25:01.491Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/f9/76c9f4d4985b5a642926162e2d41fe6019b1fa929cfa58abb7d2dc9041e5/asgiref-3.9.0-py3-none-any.whl", hash = "sha256:06a41250a0114d2b6f6a2cb3ab962147d355b53d1de15eebc34a9d04a7b79981", size = 23788, upload-time = "2025-07-03T13:24:59.115Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" }, ] [[package]] @@ -75,30 +75,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.39.3" +version = "1.39.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/42/712a74bb86d06538c55067a35b8a82c57aa303eba95b2b1ee91c829288f4/boto3-1.39.3.tar.gz", hash = "sha256:0a367106497649ae3d8a7b571b8c3be01b7b935a0fe303d4cc2574ed03aecbb4", size = 111838, upload-time = "2025-07-03T19:26:00.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/1f/b7510dcd26eb14735d6f4b2904e219b825660425a0cf0b6f35b84c7249b0/boto3-1.39.4.tar.gz", hash = "sha256:6c955729a1d70181bc8368e02a7d3f350884290def63815ebca8408ee6d47571", size = 111829, upload-time = "2025-07-09T19:23:01.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/70/723d2ab259aeaed6c96e5c1857ebe7d474ed9aa8f487dea352c60f33798f/boto3-1.39.3-py3-none-any.whl", hash = "sha256:056cfa2440fe1a157a7c2be897c749c83e1a322144aa4dad889f2fca66571019", size = 139906, upload-time = "2025-07-03T19:25:58.803Z" }, + { url = "https://files.pythonhosted.org/packages/12/5c/93292e4d8c809950c13950b3168e0eabdac828629c21047959251ad3f28c/boto3-1.39.4-py3-none-any.whl", hash = "sha256:f8e9534b429121aa5c5b7c685c6a94dd33edf14f87926e9a182d5b50220ba284", size = 139908, upload-time = "2025-07-09T19:22:59.808Z" }, ] [[package]] name = "botocore" -version = "1.39.3" +version = "1.39.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/66/96e89cc261d75f0b8125436272c335c74d2a39df84504a0c3956adcd1301/botocore-1.39.3.tar.gz", hash = "sha256:da8f477e119f9f8a3aaa8b3c99d9c6856ed0a243680aa3a3fbbfc15a8d4093fb", size = 14132316, upload-time = "2025-07-03T19:25:49.502Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/9f/21c823ea2fae3fa5a6c9e8caaa1f858acd55018e6d317505a4f14c5bb999/botocore-1.39.4.tar.gz", hash = "sha256:e662ac35c681f7942a93f2ec7b4cde8f8b56dd399da47a79fa3e370338521a56", size = 14136116, upload-time = "2025-07-09T19:22:49.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/e4/3698dbb037a44d82a501577c6e3824c19f4289f4afbcadb06793866250d8/botocore-1.39.3-py3-none-any.whl", hash = "sha256:66a81cfac18ad5e9f47696c73fdf44cdbd8f8ca51ab3fca1effca0aabf61f02f", size = 13791724, upload-time = "2025-07-03T19:25:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/58/44/f120319e0a9afface645e99f300175b9b308e4724cb400b32e1bd6eb3060/botocore-1.39.4-py3-none-any.whl", hash = "sha256:c41e167ce01cfd1973c3fa9856ef5244a51ddf9c82cb131120d8617913b6812a", size = 13795516, upload-time = "2025-07-09T19:22:44.446Z" }, ] [[package]] @@ -127,11 +127,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.7.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" }, ] [[package]] From 3956a34ba09cd2371b2b28146a0fc2ebc6a32929 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 14 Jul 2025 09:53:10 +0200 Subject: [PATCH 17/25] automerge updates to workflows --- renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 727bdf6..b72dd71 100644 --- a/renovate.json +++ b/renovate.json @@ -11,7 +11,8 @@ "matchFileNames": [ ".forgejo/workflows/*.yml", ".forgejo/workflows/*.yaml" - ] + ], + "automerge": true }, { "matchManagers": [ From 05fadd851bb0d0c67cc297cf90ee78a0b8fe2733 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 14 Jul 2025 07:55:06 +0000 Subject: [PATCH 18/25] Update https://github.com/renovatebot/github-action action to v43.0.3 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 34df542..4ea244c 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "22" - name: Renovate - uses: https://github.com/renovatebot/github-action@v43.0.2 + uses: https://github.com/renovatebot/github-action@v43.0.3 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From 934b47aadf02250482bd0838b86145418fed475d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 16 Jul 2025 03:01:48 +0000 Subject: [PATCH 19/25] Update dependency sentry-sdk to >=2.33.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e14a96b..3ad42a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "pyjwt>=2.10.1", "requests>=2.32.4", "rules>=3.5", - "sentry-sdk[django]>=2.32.0", + "sentry-sdk[django]>=2.33.0", "urlman>=2.0.2", ] diff --git a/uv.lock b/uv.lock index 05cbe97..adeeb12 100644 --- a/uv.lock +++ b/uv.lock @@ -1002,15 +1002,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.32.0" +version = "2.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/59/eb90c45cb836cf8bec973bba10230ddad1c55e2b2e9ffa9d7d7368948358/sentry_sdk-2.32.0.tar.gz", hash = "sha256:9016c75d9316b0f6921ac14c8cd4fb938f26002430ac5be9945ab280f78bec6b", size = 334932, upload-time = "2025-06-27T08:10:02.89Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/0b/6139f589436c278b33359845ed77019cd093c41371f898283bbc14d26c02/sentry_sdk-2.33.0.tar.gz", hash = "sha256:cdceed05e186846fdf80ceea261fe0a11ebc93aab2f228ed73d076a07804152e", size = 335233, upload-time = "2025-07-15T12:07:42.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/a1/fc4856bd02d2097324fb7ce05b3021fb850f864b83ca765f6e37e92ff8ca/sentry_sdk-2.32.0-py2.py3-none-any.whl", hash = "sha256:6cf51521b099562d7ce3606da928c473643abe99b00ce4cb5626ea735f4ec345", size = 356122, upload-time = "2025-06-27T08:10:01.424Z" }, + { url = "https://files.pythonhosted.org/packages/93/e5/f24e9f81c9822a24a2627cfcb44c10a3971382e67e5015c6e068421f5787/sentry_sdk-2.33.0-py2.py3-none-any.whl", hash = "sha256:a762d3f19a1c240e16c98796f2a5023f6e58872997d5ae2147ac3ed378b23ec2", size = 356397, upload-time = "2025-07-15T12:07:40.729Z" }, ] [package.optional-dependencies] @@ -1074,7 +1074,7 @@ requires-dist = [ { name = "pyjwt", specifier = ">=2.10.1" }, { name = "requests", specifier = ">=2.32.4" }, { name = "rules", specifier = ">=3.5" }, - { name = "sentry-sdk", extras = ["django"], specifier = ">=2.32.0" }, + { name = "sentry-sdk", extras = ["django"], specifier = ">=2.33.0" }, { name = "urlman", specifier = ">=2.0.2" }, ] From 83a18837ecd7b775a77557eb72c7c023fd87c904 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 28 Jul 2025 03:01:03 +0000 Subject: [PATCH 20/25] Update https://github.com/renovatebot/github-action action to v43.0.5 --- .forgejo/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yaml b/.forgejo/workflows/renovate.yaml index 4ea244c..709b132 100644 --- a/.forgejo/workflows/renovate.yaml +++ b/.forgejo/workflows/renovate.yaml @@ -19,7 +19,7 @@ jobs: node-version: "22" - name: Renovate - uses: https://github.com/renovatebot/github-action@v43.0.3 + uses: https://github.com/renovatebot/github-action@v43.0.5 with: token: ${{ secrets.RENOVATE_TOKEN }} env: From f4e8d7dcb7b1924ccbf8c54c90de0e9692993415 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Jul 2025 13:00:23 +0200 Subject: [PATCH 21/25] Make sure service cards have consistent height --- .../frontend/organizations/services.html | 6 +++--- src/servala/static/css/servala.css | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/servala/frontend/templates/frontend/organizations/services.html b/src/servala/frontend/templates/frontend/organizations/services.html index 022b6e5..cc1ecc4 100644 --- a/src/servala/frontend/templates/frontend/organizations/services.html +++ b/src/servala/frontend/templates/frontend/organizations/services.html @@ -16,10 +16,10 @@
    -
    +
    {% for service in services %}
    -
    +
    {% if service.logo %} {{ service.category }}
    -
    +
    {% if service.description %}

    {{ service.description|urlize }}

    {% endif %}
    From 5b876de18ab03090141ed4335972609902bd4fa8 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Jul 2025 13:20:25 +0200 Subject: [PATCH 25/25] Code style --- src/servala/core/models/organization.py | 2 +- .../frontend/templates/account/login.html | 29 +++++++------ .../frontend/templates/frontend/base.html | 9 +++- .../frontend/templates/frontend/profile.html | 3 +- .../templates/includes/k8s_error.html | 22 +++++----- .../frontend/templates/includes/message.html | 43 ++++++++++--------- .../frontend/templatetags/error_filters.py | 1 + src/servala/frontend/views/support.py | 2 +- 8 files changed, 59 insertions(+), 52 deletions(-) diff --git a/src/servala/core/models/organization.py b/src/servala/core/models/organization.py index 57cd660..855e4f0 100644 --- a/src/servala/core/models/organization.py +++ b/src/servala/core/models/organization.py @@ -3,8 +3,8 @@ import urlman from django.conf import settings from django.db import models, transaction from django.utils.functional import cached_property -from django.utils.text import slugify from django.utils.safestring import mark_safe +from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from django_scopes import ScopedManager, scopes_disabled diff --git a/src/servala/frontend/templates/account/login.html b/src/servala/frontend/templates/account/login.html index 233118b..58cd49b 100644 --- a/src/servala/frontend/templates/account/login.html +++ b/src/servala/frontend/templates/account/login.html @@ -1,21 +1,22 @@ {% extends "frontend/base.html" %} {% load static i18n %} {% load allauth account socialaccount %} - {% block html_title %} {% translate "Sign in" %} {% endblock html_title %} - {% block page_title %} {% translate "Welcome to Servala" %} {% endblock page_title %} - {% block card_header %} -
    - Servala +
    + Servala
    {% endblock card_header %} - {% block card_content %} {% if SOCIALACCOUNT_ENABLED %} @@ -24,9 +25,10 @@
    {% translate "Ready to get started?" %}
    -

    {% translate "Sign in to access your managed service instances and the Servala service catalog" %}

    +

    + {% translate "Sign in to access your managed service instances and the Servala service catalog" %} +

    - {% for provider in socialaccount_providers %} {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
    @@ -35,7 +37,9 @@
    @@ -43,7 +47,6 @@
    {% endif %} {% endif %} -
    - -
    diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html index c2d9285..990dc17 100644 --- a/src/servala/frontend/templates/frontend/base.html +++ b/src/servala/frontend/templates/frontend/base.html @@ -5,13 +5,18 @@ - + - {% block html_title %}Dashboard{% endblock html_title %} – Servala + + {% block html_title %} + Dashboard + {% endblock html_title %} + – Servala diff --git a/src/servala/frontend/templates/frontend/profile.html b/src/servala/frontend/templates/frontend/profile.html index dc12100..b9c1799 100644 --- a/src/servala/frontend/templates/frontend/profile.html +++ b/src/servala/frontend/templates/frontend/profile.html @@ -116,7 +116,8 @@ {% endblocktranslate %}

    - {% translate "VSHN Account Console" %} diff --git a/src/servala/frontend/templates/includes/k8s_error.html b/src/servala/frontend/templates/includes/k8s_error.html index f97474d..5707dcc 100644 --- a/src/servala/frontend/templates/includes/k8s_error.html +++ b/src/servala/frontend/templates/includes/k8s_error.html @@ -1,14 +1,12 @@ {% if show_error %} -
    - {% if has_list %} - {% if message %}{{ message }}{% endif %} -
      - {% for error in errors %} -
    • {{ error }}
    • - {% endfor %} -
    - {% else %} - {{ message }} - {% endif %} -
    +
    + {% if has_list %} + {% if message %}{{ message }}{% endif %} +
      + {% for error in errors %}
    • {{ error }}
    • {% endfor %} +
    + {% else %} + {{ message }} + {% endif %} +
    {% endif %} diff --git a/src/servala/frontend/templates/includes/message.html b/src/servala/frontend/templates/includes/message.html index d5debc9..278c009 100644 --- a/src/servala/frontend/templates/includes/message.html +++ b/src/servala/frontend/templates/includes/message.html @@ -1,28 +1,29 @@ -
    +
    {{ message }}
    - \ No newline at end of file + document.addEventListener('DOMContentLoaded', function() { + const alert = document.getElementById('auto-dismiss-alert-{{ forloop.counter0|default:' + 0 ' }}'); + if (alert && !alert.classList.contains('alert-danger')) { + setTimeout(function() { + let opacity = 1; + const fadeOutInterval = setInterval(function() { + if (opacity > 0.05) { + opacity -= 0.05; + alert.style.opacity = opacity; + } else { + clearInterval(fadeOutInterval); + const bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + } + }, 25); + }, 5000); + } + }); + diff --git a/src/servala/frontend/templatetags/error_filters.py b/src/servala/frontend/templatetags/error_filters.py index 18c4c4d..410d49a 100644 --- a/src/servala/frontend/templatetags/error_filters.py +++ b/src/servala/frontend/templatetags/error_filters.py @@ -3,6 +3,7 @@ Template filters for safe error formatting. """ import html + from django import template from django.utils.safestring import mark_safe diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py index 9181c1e..6f4c4aa 100644 --- a/src/servala/frontend/views/support.py +++ b/src/servala/frontend/views/support.py @@ -52,7 +52,7 @@ class SupportView(OrganizationViewMixin, FormView): mark_safe( _( "There was an error submitting your support request. " - "Please try again or contact us directly at servala-support@vshn.ch." + 'Please try again or contact us directly at servala-support@vshn.ch.' ) ), )