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..5707dcc
--- /dev/null
+++ b/src/servala/frontend/templates/includes/k8s_error.html
@@ -0,0 +1,12 @@
+{% if show_error %}
+
+{% endif %}
diff --git a/src/servala/frontend/templates/includes/message.html b/src/servala/frontend/templates/includes/message.html
index 59260d2..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
new file mode 100644
index 0000000..410d49a
--- /dev/null
+++ b/src/servala/frontend/templatetags/error_filters.py
@@ -0,0 +1,44 @@
+"""
+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}
")
+ else:
+ return mark_safe(f"
")
diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py
index 2d74842..f9ce50d 100644
--- a/src/servala/frontend/views/service.py
+++ b/src/servala/frontend/views/service.py
@@ -144,7 +144,12 @@ 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.organization.add_support_message(
+ _("Could not initialize service form.")
+ ),
+ )
return self.render_to_response(context)
if form.is_valid():
@@ -161,7 +166,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.organization.add_support_message(
+ _(f"Error creating instance: {str(e)}.")
+ ),
)
# If the form is not valid or if the service creation failed, we render it again
@@ -366,7 +374,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.organization.add_support_message(
+ _(f"Error updating instance: {str(e)}.")
+ ),
)
return self.form_invalid(form)
@@ -435,9 +446,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.organization.add_support_message(
+ _(
+ f"An error occurred while trying to delete instance '{self.object.name}': {str(e)}."
+ )
+ ),
)
response = HttpResponse()
response["HX-Redirect"] = str(self.object.urls.base)
diff --git a/src/servala/frontend/views/support.py b/src/servala/frontend/views/support.py
index a5de757..6f4c4aa 100644
--- a/src/servala/frontend/views/support.py
+++ b/src/servala/frontend/views/support.py
@@ -51,7 +51,8 @@ class SupportView(OrganizationViewMixin, FormView):
self.request,
mark_safe(
_(
- 'There was an error submitting your support request. Please try again or contact us directly at
servala-support@vshn.ch.'
+ "There was an error submitting your support request. "
+ 'Please try again or contact us directly at
servala-support@vshn.ch.'
)
),
)
diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css
index db9052a..ee4c080 100644
--- a/src/servala/static/css/servala.css
+++ b/src/servala/static/css/servala.css
@@ -174,3 +174,21 @@ a.btn-keycloak {
margin-top: -16px;
padding-right: 28px;
}
+
+/* Service cards equal height styling */
+.service-cards-container .card {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.service-cards-container .card-body {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+}
+
+.service-cards-container .card-footer {
+ margin-top: auto;
+}
diff --git a/uv.lock b/uv.lock
index e5236dd..adeeb12 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]]
@@ -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" },
]