From 1dbdbc7f3c5fd7ebc92b444b28375ec211b37c15 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 23 Oct 2025 22:03:39 +0000 Subject: [PATCH 1/8] Initialize diagramm_proxy module --- diagramm_proxy/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 diagramm_proxy/__init__.py diff --git a/diagramm_proxy/__init__.py b/diagramm_proxy/__init__.py new file mode 100644 index 0000000..30fabe8 --- /dev/null +++ b/diagramm_proxy/__init__.py @@ -0,0 +1 @@ +# Diagram proxy module From 020dff087177611e0b90351e42c45b178203fa5c Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 23 Oct 2025 22:04:19 +0000 Subject: [PATCH 2/8] Add diagram caching module with POST support - Implement content-based hashing for cache keys - POST diagram content to Kroki server instead of URL encoding - Store generated SVGs in local filesystem cache - Add cache clearing functionality --- diagramm_proxy/diagram_cache.py | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 diagramm_proxy/diagram_cache.py diff --git a/diagramm_proxy/diagram_cache.py b/diagramm_proxy/diagram_cache.py new file mode 100644 index 0000000..49e3b76 --- /dev/null +++ b/diagramm_proxy/diagram_cache.py @@ -0,0 +1,91 @@ +import hashlib +import os +import requests +from pathlib import Path +from django.conf import settings +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile +import logging + +logger = logging.getLogger(__name__) + +# Configure cache directory +CACHE_DIR = getattr(settings, 'DIAGRAM_CACHE_DIR', 'diagram_cache') +KROKI_UPSTREAM = "http://svckroki:8000" + +def get_cache_path(diagram_type, content_hash): + """Generate cache file path for a diagram.""" + return os.path.join(CACHE_DIR, diagram_type, f"{content_hash}.svg") + +def compute_hash(content): + """Compute SHA256 hash of diagram content.""" + return hashlib.sha256(content.encode('utf-8')).hexdigest() + +def get_cached_diagram(diagram_type, diagram_content): + """ + Retrieve diagram from cache or generate it via POST. + + Args: + diagram_type: Type of diagram (e.g., 'plantuml', 'mermaid') + diagram_content: Raw diagram content + + Returns: + Path to cached diagram file (relative to MEDIA_ROOT) + """ + content_hash = compute_hash(diagram_content) + cache_path = get_cache_path(diagram_type, content_hash) + + # Check if diagram exists in cache + if default_storage.exists(cache_path): + logger.debug(f"Cache hit for {diagram_type} diagram: {content_hash[:8]}") + return cache_path + + # Generate diagram via POST request + logger.info(f"Cache miss for {diagram_type} diagram: {content_hash[:8]}, generating...") + try: + url = f"{KROKI_UPSTREAM}/{diagram_type}/svg" + response = requests.post( + url, + data=diagram_content.encode('utf-8'), + headers={'Content-Type': 'text/plain'}, + timeout=30 + ) + response.raise_for_status() + + # Ensure cache directory exists + cache_dir = os.path.dirname(default_storage.path(cache_path)) + os.makedirs(cache_dir, exist_ok=True) + + # Save to cache + default_storage.save(cache_path, ContentFile(response.content)) + logger.info(f"Diagram cached successfully: {cache_path}") + + return cache_path + + except requests.RequestException as e: + logger.error(f"Error generating diagram: {e}") + raise + +def clear_cache(diagram_type=None): + """ + Clear cached diagrams. + + Args: + diagram_type: If specified, only clear diagrams of this type + """ + if diagram_type: + cache_path = os.path.join(CACHE_DIR, diagram_type) + else: + cache_path = CACHE_DIR + + if default_storage.exists(cache_path): + full_path = default_storage.path(cache_path) + # Walk through and delete files + for root, dirs, files in os.walk(full_path): + for file in files: + file_path = os.path.join(root, file) + try: + os.remove(file_path) + logger.info(f"Deleted cached diagram: {file_path}") + except OSError as e: + logger.error(f"Error deleting {file_path}: {e}") From cd7195b3aaada653c7f96f2e9e713ce3147098cf Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 23 Oct 2025 22:05:13 +0000 Subject: [PATCH 3/8] Update diagram rendering to use POST with caching - Replace URL-encoded GET approach with POST requests - Use local filesystem cache for generated diagrams - Add error handling with fallback message - Serve diagrams from MEDIA_URL instead of proxy --- abschnitte/utils.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/abschnitte/utils.py b/abschnitte/utils.py index 6194727..6d13cf4 100644 --- a/abschnitte/utils.py +++ b/abschnitte/utils.py @@ -3,6 +3,10 @@ import base64 import zlib import re from textwrap import dedent +from django.conf import settings + +# Import the caching function +from diagramm_proxy.diagram_cache import get_cached_diagram DIAGRAMMSERVER="/diagramm" @@ -25,15 +29,23 @@ def render_textabschnitte(queryset): elif typ == "tabelle": html = md_table_to_html(inhalt) elif typ == "diagramm": - temp=inhalt.splitlines() - diagramtype=temp.pop(0) - diagramoptions='width="100%"' - if temp[0][0:6].lower() == "option": - diagramoptions=temp.pop(0).split(":",1)[1] - rest="\n".join(temp) - html = '

' + except Exception as e: + # Fallback to error message + html = f'

Error generating diagram: {str(e)}

' + elif typ == "code": html = "
"
             html += markdown(inhalt, extensions=['tables', 'attr_list'])

From 8f57f5fc5bea214cd18d25968fbd675717ff320a Mon Sep 17 00:00:00 2001
From: "Adrian A. Baumann" 
Date: Thu, 23 Oct 2025 22:05:21 +0000
Subject: [PATCH 4/8] Add management module

---
 abschnitte/management/__init__.py | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 abschnitte/management/__init__.py

diff --git a/abschnitte/management/__init__.py b/abschnitte/management/__init__.py
new file mode 100644
index 0000000..2c1c7c1
--- /dev/null
+++ b/abschnitte/management/__init__.py
@@ -0,0 +1 @@
+# Management commands

From 1ee9b3c46f3fd1d82ce13b38ea617877b8a9dc9e Mon Sep 17 00:00:00 2001
From: "Adrian A. Baumann" 
Date: Thu, 23 Oct 2025 22:05:26 +0000
Subject: [PATCH 5/8] Add commands package

---
 abschnitte/management/commands/__init__.py | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 abschnitte/management/commands/__init__.py

diff --git a/abschnitte/management/commands/__init__.py b/abschnitte/management/commands/__init__.py
new file mode 100644
index 0000000..b5a3a84
--- /dev/null
+++ b/abschnitte/management/commands/__init__.py
@@ -0,0 +1 @@
+# Commands package

From 966cd4622823f46bf3dc730696189fa42d0dcd5b Mon Sep 17 00:00:00 2001
From: "Adrian A. Baumann" 
Date: Thu, 23 Oct 2025 22:05:36 +0000
Subject: [PATCH 6/8] Add management command to clear diagram cache

Usage:
  python manage.py clear_diagram_cache
  python manage.py clear_diagram_cache --type plantuml
---
 .../commands/clear_diagram_cache.py           | 23 +++++++++++++++++++
 1 file changed, 23 insertions(+)
 create mode 100644 abschnitte/management/commands/clear_diagram_cache.py

diff --git a/abschnitte/management/commands/clear_diagram_cache.py b/abschnitte/management/commands/clear_diagram_cache.py
new file mode 100644
index 0000000..035b466
--- /dev/null
+++ b/abschnitte/management/commands/clear_diagram_cache.py
@@ -0,0 +1,23 @@
+from django.core.management.base import BaseCommand
+from diagramm_proxy.diagram_cache import clear_cache
+
+
+class Command(BaseCommand):
+    help = 'Clear cached diagrams'
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '--type',
+            type=str,
+            help='Diagram type to clear (e.g., plantuml, mermaid)',
+        )
+
+    def handle(self, *args, **options):
+        diagram_type = options.get('type')
+        if diagram_type:
+            self.stdout.write(f'Clearing cache for {diagram_type}...')
+            clear_cache(diagram_type)
+        else:
+            self.stdout.write('Clearing all diagram caches...')
+            clear_cache()
+        self.stdout.write(self.style.SUCCESS('Cache cleared successfully'))

From dbb3ecd5bf9a0d1a6ff0c09b3a81687683f958d4 Mon Sep 17 00:00:00 2001
From: "Adrian A. Baumann" 
Date: Thu, 23 Oct 2025 22:06:07 +0000
Subject: [PATCH 7/8] Add diagram cache directory to gitignore

---
 .gitignore | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.gitignore b/.gitignore
index a272658..406784b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@ keys/
 .idea/
 
 *.kate-swp
+
+# Diagram cache directory
+media/diagram_cache/

From 67c393ecf19143b3f06ba3573dcf9510f6b06ee2 Mon Sep 17 00:00:00 2001
From: "Adrian A. Baumann" 
Date: Thu, 23 Oct 2025 22:06:30 +0000
Subject: [PATCH 8/8] Add documentation for diagram POST caching feature

---
 Documentation/DIAGRAM_CACHING.md | 105 +++++++++++++++++++++++++++++++
 1 file changed, 105 insertions(+)
 create mode 100644 Documentation/DIAGRAM_CACHING.md

diff --git a/Documentation/DIAGRAM_CACHING.md b/Documentation/DIAGRAM_CACHING.md
new file mode 100644
index 0000000..4effbbf
--- /dev/null
+++ b/Documentation/DIAGRAM_CACHING.md
@@ -0,0 +1,105 @@
+# Diagram POST Caching Implementation
+
+This feature replaces the URL-encoded GET approach for diagram generation with POST requests and local filesystem caching.
+
+## Changes Overview
+
+### New Files
+- `diagramm_proxy/__init__.py` - Module initialization
+- `diagramm_proxy/diagram_cache.py` - Caching logic and POST request handling
+- `abschnitte/management/commands/clear_diagram_cache.py` - Management command for cache clearing
+
+### Modified Files
+- `abschnitte/utils.py` - Updated `render_textabschnitte()` to use caching
+- `.gitignore` - Added cache directory exclusion
+
+## Configuration Required
+
+Add to your Django settings file (e.g., `VorgabenUI/settings.py`):
+
+```python
+# Diagram cache settings
+DIAGRAM_CACHE_DIR = 'diagram_cache'  # relative to MEDIA_ROOT
+
+# Ensure MEDIA_ROOT and MEDIA_URL are configured
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+MEDIA_URL = '/media/'
+```
+
+### URL Configuration
+
+Ensure media files are served in development. In your main `urls.py`:
+
+```python
+from django.conf import settings
+from django.conf.urls.static import static
+
+# ... existing urlpatterns ...
+
+# Serve media files in development
+if settings.DEBUG:
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+```
+
+## How It Works
+
+1. When a diagram is rendered, the system computes a SHA256 hash of the diagram content
+2. It checks if a cached SVG exists for that hash
+3. If cached: serves the existing file
+4. If not cached: POSTs content to Kroki server, saves the response, and serves it
+5. Diagrams are served from `MEDIA_URL/diagram_cache/{type}/{hash}.svg`
+
+## Benefits
+
+- **No URL length limitations** - Content is POSTed instead of URL-encoded
+- **Improved performance** - Cached diagrams are served directly from filesystem
+- **Reduced server load** - Kroki server is only called once per unique diagram
+- **Persistent cache** - Survives application restarts
+- **Better error handling** - Graceful fallback on generation failures
+
+## Usage
+
+### Viewing Diagrams
+No changes required - diagrams will be automatically cached on first render.
+
+### Clearing Cache
+
+Clear all cached diagrams:
+```bash
+python manage.py clear_diagram_cache
+```
+
+Clear diagrams of a specific type:
+```bash
+python manage.py clear_diagram_cache --type plantuml
+python manage.py clear_diagram_cache --type mermaid
+```
+
+## Testing
+
+1. Create or view a page with diagrams
+2. Verify diagrams render correctly
+3. Check that `media/diagram_cache/` directory is created with cached SVGs
+4. Refresh the page - second load should be faster (cache hit)
+5. Check logs for cache hit/miss messages
+6. Test cache clearing command
+
+## Migration Notes
+
+- Existing diagrams will be regenerated on first view after deployment
+- The old URL-based approach is completely replaced
+- No database migrations needed
+- Ensure `requests` library is installed (already in requirements.txt)
+
+## Troubleshooting
+
+### Diagrams not rendering
+- Check that MEDIA_ROOT and MEDIA_URL are configured correctly
+- Verify Kroki server is accessible at `http://svckroki:8000`
+- Check application logs for error messages
+- Ensure media directory is writable
+
+### Cache not working
+- Verify Django storage configuration
+- Check file permissions on media/diagram_cache directory
+- Review logs for cache-related errors