feat(F-G-H-I): STL cache, invoices, import validation, notification settings

Phase F — STL Hash Cache:
- Migration 041: step_file_hash column on cad_files
- cache_service.py: SHA256 hash + MinIO-backed STL cache (check/store)
- render_step_thumbnail: compute+persist hash before render
- generate_stl_cache: check MinIO cache before cadquery conversion, store after

Phase G — Invoices:
- Migration 042: invoices + invoice_lines tables with RLS
- Invoice/InvoiceLine models + schemas
- billing service: generate_invoice_number (INV-YYYY-NNNN), create/list/get/delete/PDF
- WeasyPrint PDF generation; backend Dockerfile + pyproject.toml deps
- invoice_router with 6 endpoints; registered in main.py
- frontend: Billing.tsx page + api/billing.ts; route + nav link

Phase H — Import Sanity Check:
- Migration 043: import_validations table
- ImportValidation model + schemas
- run_sanity_check: material fuzzy-match (cutoff=0.8), STEP availability, duplicate detection
- validate_excel_import Celery task (queue: step_processing)
- uploads.py: create ImportValidation on /excel, fire task, expose GET /validations/{id}
- frontend: Upload.tsx polling ValidationDialog with Ampel status indicators

Phase I — Notification Settings:
- Migration 044: notification_configs table (user×event×channel toggles)
- NotificationConfig model + seeds (in_app=true, email=false)
- get/upsert/reset config endpoints on /notifications/config
- frontend: NotificationSettings.tsx page + api/notifications.ts extensions

Infrastructure:
- docker-compose.yml: add worker-thumbnail service (concurrency=1, Q=thumbnail_rendering)
- Fix Dockerfile: libgdk-pixbuf-2.0-0 (correct Debian bookworm package name)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 18:05:01 +01:00
parent 7706c514c8
commit f19a6ccde8
34 changed files with 1940 additions and 14 deletions
+1
View File
@@ -10,6 +10,7 @@ celery_app = Celery(
"app.tasks.ai_tasks",
"app.domains.rendering.tasks",
"app.domains.products.tasks",
"app.domains.imports.tasks",
],
)
+34 -1
View File
@@ -132,6 +132,28 @@ def render_step_thumbnail(self, cad_file_id: str):
On success, also auto-populates materials and marks the CadFile as completed.
"""
logger.info(f"Rendering thumbnail for CAD file: {cad_file_id}")
# Compute and persist STEP file hash for STL cache lookups
try:
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from app.config import settings as app_settings
from app.models.cad_file import CadFile
from app.domains.products.cache_service import compute_step_hash
sync_url = app_settings.database_url.replace("+asyncpg", "")
_eng = create_engine(sync_url)
with Session(_eng) as _sess:
_cad = _sess.get(CadFile, cad_file_id)
if _cad and _cad.stored_path and not _cad.step_file_hash:
_hash = compute_step_hash(_cad.stored_path)
_cad.step_file_hash = _hash
_sess.commit()
logger.info(f"Saved step_file_hash for {cad_file_id}: {_hash[:12]}")
_eng.dispose()
except Exception:
logger.warning(f"step_file_hash computation failed for {cad_file_id} (non-fatal)")
try:
from app.services.step_processor import regenerate_cad_thumbnail
success = regenerate_cad_thumbnail(cad_file_id, part_colors={})
@@ -172,13 +194,24 @@ def generate_stl_cache(self, cad_file_id: str, quality: str):
try:
from app.services.render_blender import convert_step_to_stl, export_per_part_stls
from app.domains.products.cache_service import compute_step_hash, check_stl_cache, store_stl_cache
from pathlib import Path as _Path
step = _Path(step_path)
stl_out = step.parent / f"{step.stem}_{quality}.stl"
parts_dir = step.parent / f"{step.stem}_{quality}_parts"
if not stl_out.exists() or stl_out.stat().st_size == 0:
convert_step_to_stl(step, stl_out, quality)
# Check MinIO cache before running cadquery conversion
step_hash = compute_step_hash(step_path)
cached_bytes = check_stl_cache(step_hash, quality)
if cached_bytes:
stl_out.write_bytes(cached_bytes)
logger.info(f"STL cache hit for {cad_file_id} ({quality}), skipped conversion")
else:
convert_step_to_stl(step, stl_out, quality)
# Store result in MinIO for future workers
if stl_out.exists() and stl_out.stat().st_size > 0:
store_stl_cache(step_hash, quality, str(stl_out))
if not (parts_dir / "manifest.json").exists():
try:
export_per_part_stls(step, parts_dir, quality)