diff --git a/app/database.py b/app/database.py
index 5a45706..a1daa06 100755
--- a/app/database.py
+++ b/app/database.py
@@ -85,6 +85,21 @@ def clean_source_label(source: Optional[str]) -> str:
return src
+def clean_instructions_text(text: Optional[str]) -> str:
+ """Entfernt vorangestellte Schrittnummern (z. B. '1. ', 'Schritt 1: ') aus Zubereitungszeilen."""
+ if not text:
+ return ""
+ import re
+ cleaned_lines = []
+ for line in str(text).splitlines():
+ line_s = line.strip()
+ if not line_s:
+ continue
+ cleaned = re.sub(r'^(?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*', '', line_s, flags=re.IGNORECASE)
+ cleaned_lines.append(cleaned)
+ return "\n".join(cleaned_lines)
+
+
def guess_category_for_ingredient(name: str) -> str:
"""Bestimmt automatisch die passende Supermarkt-Kategorie anhand deutscher Zutaten-Keywords."""
if not name:
@@ -713,6 +728,8 @@ def add_recipe(
# Fallback to mapped chapter number if possible
chapter_number = CHAPTER_MAP.get(category, 1)
+ instructions = clean_instructions_text(instructions)
+
res = execute_query(
"""
INSERT INTO recipes (title, category, chapter_number, effort, prep_time, cook_time, portions, has_portion_table, ingredients, instructions, notes, source, image_url, printed_at)
@@ -755,6 +772,8 @@ def update_recipe(
if not existing:
raise KeyError(f"Rezept #{recipe_id} nicht gefunden.")
+ instructions = clean_instructions_text(instructions)
+
use_portion_table = int(bool(has_portion_table)) if has_portion_table is not None else existing.get("has_portion_table", 0)
if image_url is not None:
diff --git a/app/main.py b/app/main.py
index f95068b..2bfea00 100755
--- a/app/main.py
+++ b/app/main.py
@@ -130,6 +130,14 @@ async def auth_middleware(request: Request, call_next):
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
templates = Jinja2Templates(directory=TEMPLATES_DIR)
+def strip_step_number_filter(text: str) -> str:
+ if not text:
+ return ""
+ import re
+ return re.sub(r'^(?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*', '', str(text).strip(), flags=re.IGNORECASE)
+
+templates.env.filters["strip_step_number"] = strip_step_number_filter
+
# Mapping Kochbuch-Kapitel -> Zufallsgenerator-Kategorien
CHAPTER_TO_DISH_CAT = {
"Suppen & Eintöpfe": "Suppen",
diff --git a/app/templates/cookbook.html b/app/templates/cookbook.html
index e68c160..b2304f7 100755
--- a/app/templates/cookbook.html
+++ b/app/templates/cookbook.html
@@ -803,7 +803,7 @@
{% if step.strip() %}
{{ loop.index }}
-
{{ step.strip() }}
+
{{ step.strip() | strip_step_number }}
{% endif %}
{% endfor %}
diff --git a/app/templates/recipe_detail.html b/app/templates/recipe_detail.html
index 14ee2c6..d37f0ab 100755
--- a/app/templates/recipe_detail.html
+++ b/app/templates/recipe_detail.html
@@ -181,7 +181,7 @@
{% if step.strip() %}
{{ loop.index }}
-
{{ step.strip() }}
+
{{ step.strip() | strip_step_number }}
{% endif %}
{% endfor %}
diff --git a/scripts/preview_strip_numbers.py b/scripts/preview_strip_numbers.py
new file mode 100644
index 0000000..ce728b6
--- /dev/null
+++ b/scripts/preview_strip_numbers.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import os
+import sys
+import re
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from app.database import get_connection
+
+sys.stdout.reconfigure(encoding='utf-8')
+
+conn = get_connection()
+cur = conn.cursor()
+cur.execute("SELECT id, title, instructions FROM recipes ORDER BY id")
+recipes = cur.fetchall()
+
+def clean_instructions(text: str) -> str:
+ cleaned_lines = []
+ for line in text.split("\n"):
+ line_s = line.strip()
+ if not line_s:
+ continue
+ cleaned = re.sub(r'^(?:Schritt\s*)?\d+[\.\)\:\-]?\s*', '', line_s, flags=re.IGNORECASE)
+ cleaned_lines.append(cleaned)
+ return "\n".join(cleaned_lines)
+
+print(f"Total recipes to preview: {len(recipes)}")
+for r in recipes[:10]:
+ lines_before = [l.strip() for l in r['instructions'].split('\n') if l.strip()]
+ lines_after = [l.strip() for l in clean_instructions(r['instructions']).split('\n') if l.strip()]
+ print(f"\n=== ID {r['id']}: {r['title']} ===")
+ for b, a in zip(lines_before[:2], lines_after[:2]):
+ print(f" VORHER: {b[:65]}")
+ print(f" NACHHER: {a[:65]}")
+
+conn.close()
diff --git a/scripts/strip_step_numbers_db.py b/scripts/strip_step_numbers_db.py
new file mode 100644
index 0000000..a27e169
--- /dev/null
+++ b/scripts/strip_step_numbers_db.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Strip leading step numbers from instructions in MariaDB to avoid duplicate numbers in the UI.
+"""
+import os
+import sys
+import re
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from app.database import get_connection
+
+sys.stdout.reconfigure(encoding='utf-8')
+
+STEP_REGEX = re.compile(r'^(?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*', re.IGNORECASE)
+
+def clean_instructions_text(text: str) -> str:
+ if not text:
+ return ""
+ cleaned_lines = []
+ for line in text.split("\n"):
+ line_s = line.strip()
+ if not line_s:
+ continue
+ cleaned = STEP_REGEX.sub('', line_s)
+ cleaned_lines.append(cleaned)
+ return "\n".join(cleaned_lines)
+
+def main():
+ conn = get_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT id, title, instructions FROM recipes ORDER BY id")
+ recipes = cur.fetchall()
+
+ updated_count = 0
+ total_lines_cleaned = 0
+
+ for r in recipes:
+ original = r['instructions'] or ""
+ cleaned = clean_instructions_text(original)
+ if cleaned != original:
+ cur.execute("UPDATE recipes SET instructions = %s WHERE id = %s", (cleaned, r['id']))
+ updated_count += 1
+ # count cleaned lines
+ for orig_line in original.split("\n"):
+ if STEP_REGEX.match(orig_line.strip()):
+ total_lines_cleaned += 1
+
+ conn.commit()
+ conn.close()
+
+ print(f"Successfully cleaned instructions across {updated_count} of {len(recipes)} recipes.")
+ print(f"Total step number prefixes removed: {total_lines_cleaned}.")
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/test_endpoints.py b/scripts/test_endpoints.py
index 615f3b9..d97be65 100644
--- a/scripts/test_endpoints.py
+++ b/scripts/test_endpoints.py
@@ -33,6 +33,14 @@ def test_routes():
assert res.status_code == 200, f"Recipe 1 failed: {res.status_code}"
print("✓ Recipe detail (/cookbook/recipe/1) OK")
+ # Verify no double numbers in /cookbook
+ import re
+ res_cb = client.get("/cookbook")
+ pattern = re.compile(r'\s*((?:Schritt\s*\d+[:.]?|\d+[\.\)\:])\s*[^<]*)', re.IGNORECASE)
+ matches = pattern.findall(res_cb.text)
+ assert len(matches) == 0, f"Found {len(matches)} double numbers in /cookbook: {matches[:3]}"
+ print("✓ Zero double numbers in rendered /cookbook steps confirmed!")
+
print("\nAll route tests passed successfully!")
if __name__ == "__main__":