diff --git a/.continue/agents/new-config.yaml b/.continue/agents/new-config.yaml deleted file mode 100644 index f45d8c2..0000000 --- a/.continue/agents/new-config.yaml +++ /dev/null @@ -1,75 +0,0 @@ -name: Local Qwen Setup - -models: - - name: Qwen3.5 Chat - provider: openai - model: qwen - apiBase: http://localhost:8000/v1 - apiKey: none - roles: - - chat - - edit - - apply - defaultCompletionOptions: - temperature: 0.1 - top_p: 0.9 - max_tokens: 1024 - stop: - - "" - - - name: Qwen3.5 Autocomplete - provider: openai - model: qwen - apiBase: http://localhost:8000/v1 - apiKey: none - roles: - - autocomplete - defaultCompletionOptions: - temperature: 0.05 - max_tokens: 256 - -context: - - provider: code - - provider: docs - - provider: diff - - provider: terminal - -slashCommands: - - name: fix - description: Corriger un bug de manière minimale - prompt: | - Corrige uniquement le problème identifié. - Contraintes: - - modification minimale - - pas de refactor global - - conserve l'architecture existante - - - name: test - description: Générer un test pytest - prompt: | - Écris un test pytest minimal qui reproduit ce bug. - Ne corrige pas le code. - - - name: explain - description: Expliquer du code - prompt: | - Explique ce code de manière concise et technique. - - - name: improve - description: Amélioration contrôlée - prompt: | - Propose une amélioration ciblée. - Ne fais pas de refactor global. - -systemMessage: | - Tu es un expert en développement logiciel. - Spécialités: - - Python / FastAPI - - SIG / géospatial - - traitement de nuages de points (PDAL, Potree) - - Règles strictes: - - ne jamais refactoriser massivement - - toujours proposer des modifications minimales - - privilégier des solutions robustes et simples - - si incertain, poser une question au lieu d'inventer diff --git a/.gitignore b/.gitignore deleted file mode 100644 index dec50f5..0000000 --- a/.gitignore +++ /dev/null @@ -1,77 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# Virtual Environment -venv/ -ENV/ -env/ -.venv - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# Docker -Dockerfile.builder -Dockerfile.entwine -Dockerfile.frontend -docker-compose.yml - -# Répertoires Potree -backend/static/potree/ -frontend/static/potree/ - -# Données LiDAR (fichiers volumineux) -backend/data/ept/*/ept-data/*.laz -backend/data/ept/*/ept-data/*.las -backend/data/uploads/*.laz -backend/data/uploads/*.las -backend/data/uploads/delete.txt - -# Fichiers de données temporaires -backend/data/ept/*/*.json -backend/data/ept/*/*/*.json -backend/data/ept/*/*/*/*.json - -# Logs -*.log -logs/ - -# Fichiers temporaires -*.tmp -*.temp -.DS_Store -Thumbs.db - -# Fichiers de configuration locale -.env -.env.local -.env.*.local - -# Fichiers générés -*.pkl -*.h5 -*.npz diff --git a/backend/__init__.py b/backend/__init__.py index e69de29..06d7405 100644 Binary files a/backend/__init__.py and b/backend/__init__.py differ diff --git a/frontend/routes/admin.py b/frontend/routes/admin.py index c54f04c..0fc7631 100644 --- a/frontend/routes/admin.py +++ b/frontend/routes/admin.py @@ -25,47 +25,54 @@ async def save_backend_config(request: Request): form_data = await request.form() new_backend_url = form_data.get("backend_url", "").strip().rstrip("/") new_potree_url = form_data.get("potree_url", "").strip().rstrip("/") - + if not new_backend_url: return request.app.state.templates.TemplateResponse( "partials/backend_config.html", {"request": request, "current_backend_url": config.BACKEND_URL, "current_potree_url": config.POTREE_URL, "error": "URL du backend vide"}, ) - - # Sauvegarder dans le fichier JSON + config_file = Path(__file__).parent.parent / "config" / "backend.json" config_file.parent.mkdir(parents=True, exist_ok=True) - + config_data = {} if new_backend_url: config_data["backend_url"] = new_backend_url if new_potree_url: config_data["potree_url"] = new_potree_url - + with open(config_file, "w") as f: json.dump(config_data, f, indent=2) - - # Mettre à jour les variables module + config.BACKEND_URL = new_backend_url config.POTREE_URL = new_potree_url - + return request.app.state.templates.TemplateResponse( "partials/backend_config.html", {"request": request, "current_backend_url": new_backend_url, "current_potree_url": new_potree_url, "success": True}, ) -@router.get("/list", response_class=HTMLResponse) +@router.get("/admin/list", response_class=HTMLResponse) async def admin_list(request: Request): - """Affiche la liste de tous les nuages de points""" - pointclouds = await api_client.list_pointclouds() + """ + Liste via l'api_client (tab Admin). + Retourne cloud_list_body.html pour injection dans #cloud-list-body. + """ + try: + pointclouds = await api_client.list_pointclouds() + except Exception as e: + return request.app.state.templates.TemplateResponse( + "partials/cloud_list_body.html", + {"request": request, "pointclouds": [], "error": str(e)}, + ) return request.app.state.templates.TemplateResponse( - "partials/cloud_list.html", + "partials/cloud_list_body.html", {"request": request, "pointclouds": pointclouds}, ) -@router.get("/debug/{pc_id}", response_class=HTMLResponse) +@router.get("/admin/debug/{pc_id}", response_class=HTMLResponse) async def admin_debug(request: Request, pc_id: str): """Affiche les informations de debug pour un nuage""" debug_info = await api_client.get_debug(pc_id) @@ -75,40 +82,62 @@ async def admin_debug(request: Request, pc_id: str): ) -@router.delete("/delete/{pc_id}", response_class=HTMLResponse) +@router.delete("/admin/delete/{pc_id}", response_class=HTMLResponse) async def admin_delete(request: Request, pc_id: str): - """Supprime un nuage de points""" + """Supprime un nuage de points et rafraîchit la liste""" try: - result = await api_client.delete_pointcloud(pc_id) - return request.app.state.templates.TemplateResponse( - "partials/upload_result.html", - {"request": request, "result": { - "id": pc_id, - "filename": f"{pc_id}.las", - "size_mb": 0, - "conversion_time_seconds": 0, - }, "error": None}, - ) + await api_client.delete_pointcloud(pc_id) except Exception as e: return request.app.state.templates.TemplateResponse( - "partials/upload_result.html", - {"request": request, "error": str(e), "result": None}, + "partials/cloud_list_body.html", + {"request": request, "pointclouds": [], "error": f"Erreur suppression : {str(e)}"}, ) + # Après suppression, on retourne la liste mise à jour + from pathlib import Path as _Path + pointclouds = [] + ept_dir = _Path(config.EPT_DIR) + if ept_dir.exists(): + for item in sorted(ept_dir.iterdir(), key=lambda x: x.stat().st_ctime, reverse=True): + if item.is_dir(): + total_size = sum(f.stat().st_size for f in item.rglob("*") if f.is_file()) + file_count = sum(1 for f in item.rglob("*") if f.is_file()) + if file_count > 0: + pointclouds.append({ + "id": item.name, + "size_mb": round(total_size / (1024 * 1024), 2), + "file_count": file_count, + "manifest": {}, + "created": item.stat().st_ctime, + }) + + return request.app.state.templates.TemplateResponse( + "partials/cloud_list_body.html", + {"request": request, "pointclouds": pointclouds}, + ) + + +@router.post("/admin/crop/{pc_id}", response_class=HTMLResponse) +async def admin_crop(request: Request, pc_id: str): + """Crop via api_client — lit le body form (appelé depuis crop_section.html)""" + form_data = await request.form() + try: + box = { + "minX": float(form_data.get("minX", 0)), + "minY": float(form_data.get("minY", 0)), + "minZ": float(form_data.get("minZ", 0)), + "maxX": float(form_data.get("maxX", 0)), + "maxY": float(form_data.get("maxY", 0)), + "maxZ": float(form_data.get("maxZ", 0)), + } + except (TypeError, ValueError) as e: + return request.app.state.templates.TemplateResponse( + "partials/upload_result.html", + {"request": request, "error": f"Coordonnées invalides : {str(e)}", "result": None}, + ) -@router.post("/crop/{pc_id}", response_class=HTMLResponse) -async def admin_crop(request: Request, pc_id: str, box: dict): - """ - Crop le nuage de points avec PDAL. - - Args: - pc_id: ID du nuage de points à cropper - box: dict avec les coordonnées de la box 3D - {"minX", "minY", "minZ", "maxX", "maxY", "maxZ"} - """ try: result = await api_client.crop_pointcloud(pc_id, box) - if result.get("ok"): return request.app.state.templates.TemplateResponse( "partials/upload_result.html", diff --git a/frontend/routes/crop.py b/frontend/routes/crop.py index d52651c..3c225da 100644 --- a/frontend/routes/crop.py +++ b/frontend/routes/crop.py @@ -1,6 +1,7 @@ -from fastapi import APIRouter, Request, HTTPException +# routes/crop.py + +from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse -import api_client import config router = APIRouter() @@ -8,44 +9,11 @@ router = APIRouter() @router.get("/crop", response_class=HTMLResponse) async def crop_ui(request: Request, pc_id: str): - """Interface utilisateur pour le crop du nuage de points""" - embed_url = f"{config.BACKEND_URL}/viewer-embed/{pc_id}" + """ + Retourne le partial crop_section.html pour injection dans #crop-panel. + Appelé par le bouton ✂️ dans cloud_list_body.html. + """ return request.app.state.templates.TemplateResponse( - "partials/crop.html", - {"request": request, "pc_id": pc_id, "embed_url": embed_url}, - ) - - -@router.post("/crop", response_class=HTMLResponse) -async def crop(request: Request, pc_id: str, box: dict): - """ - Traite la requête de crop : - 1. Envoie la box 3D au backend - 2. Le backend utilise PDAL pour cropper le nuage - 3. Retourne le nouveau pc_id pour l'affichage - """ - try: - # Envoi de la requête de crop au backend - result = await api_client.crop_pointcloud(pc_id, box) - - if result.get("ok"): - new_pc_id = result.get("id") - return request.app.state.templates.TemplateResponse( - "partials/upload_result.html", - {"request": request, "result": { - "id": new_pc_id, - "filename": f"{pc_id}_cropped.las", - "size_mb": result.get("size_mb", 0), - "conversion_time_seconds": result.get("conversion_time_seconds", 0), - }, "error": None}, - ) - else: - return request.app.state.templates.TemplateResponse( - "partials/upload_result.html", - {"request": request, "error": result.get("detail", "Erreur inconnue"), "result": None}, - ) - except Exception as e: - return request.app.state.templates.TemplateResponse( - "partials/upload_result.html", - {"request": request, "error": str(e), "result": None}, - ) \ No newline at end of file + "partials/crop_section.html", + {"request": request, "pc_id": pc_id}, + ) \ No newline at end of file diff --git a/frontend/routes/upload.py b/frontend/routes/upload.py index c0fbfbb..040edc3 100644 --- a/frontend/routes/upload.py +++ b/frontend/routes/upload.py @@ -17,6 +17,17 @@ async def root(): @router.get("/upload", response_class=HTMLResponse) async def index(request: Request): + """ + ✅ CORRECTION BUG 2 : si la requête vient de HTMX (header HX-Request), + on retourne uniquement le partial upload_form.html (sans navbar, sans layout). + Si c'est une navigation directe dans le navigateur, on retourne index.html complet. + """ + is_htmx = request.headers.get("HX-Request") == "true" + if is_htmx: + return request.app.state.templates.TemplateResponse( + "partials/upload_form.html", + {"request": request}, + ) return request.app.state.templates.TemplateResponse( "index.html", {"request": request, "active_tab": "upload"}, diff --git a/frontend/routes/viewer.py b/frontend/routes/viewer.py index dba7780..6f9e8d7 100644 --- a/frontend/routes/viewer.py +++ b/frontend/routes/viewer.py @@ -3,8 +3,6 @@ from fastapi.responses import HTMLResponse import config import api_client from pathlib import Path -import shutil -import os from datetime import datetime router = APIRouter() @@ -12,42 +10,35 @@ router = APIRouter() @router.get("/viewer/list", response_class=HTMLResponse) async def viewer_list(request: Request): - """Liste les nuages de points disponibles - Endpoint autonome frontend""" + """ + Liste les nuages de points — retourne uniquement le contenu intérieur + (cloud_list_body.html) pour injection dans #cloud-list-body. + La card enveloppe et le bouton Actualiser vivent dans index.html, pas ici. + """ pointclouds = [] - - # Récupérer la liste des nuages directement depuis le système de fichiers + ept_dir = Path(config.EPT_DIR) if ept_dir.exists(): for item in sorted(ept_dir.iterdir(), key=lambda x: x.stat().st_ctime, reverse=True): if item.is_dir(): - # Lire le manifeste - manifest_path = item / "manifest.json" - manifest = {} - if manifest_path.exists(): - try: - manifest = {"ept_dir": item.name} - except: - pass - - # Calculer la taille et le nombre de fichiers total_size = 0 file_count = 0 for f in item.rglob("*"): if f.is_file(): total_size += f.stat().st_size file_count += 1 - + if file_count > 0: pointclouds.append({ "id": item.name, "size_mb": round(total_size / (1024 * 1024), 2), "file_count": file_count, - "manifest": manifest, + "manifest": {}, "created": item.stat().st_ctime, }) - + return request.app.state.templates.TemplateResponse( - "partials/cloud_list.html", + "partials/cloud_list_body.html", {"request": request, "pointclouds": pointclouds}, ) @@ -58,4 +49,4 @@ async def viewer(request: Request, pc_id: str): return request.app.state.templates.TemplateResponse( "partials/viewer.html", {"request": request, "pc_id": pc_id, "embed_url": embed_url}, - ) + ) \ No newline at end of file diff --git a/frontend/templates/index.html b/frontend/templates/index.html index 20671cb..2525998 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -10,110 +10,137 @@ - -