diff --git a/backend/main.py b/backend/main.py index 488430a..be114a7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,7 +7,7 @@ from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, FileResponse, JSONResponse from fastapi.exceptions import RequestValidationError from sqlalchemy.orm import Session -from typing import List, Dict +from typing import List, Dict, Optional from pydantic import BaseModel # Configure logging @@ -158,11 +158,24 @@ async def sound_level_meters_page(request: Request): @app.get("/slm/{unit_id}", response_class=HTMLResponse) -async def slm_legacy_dashboard(request: Request, unit_id: str): +async def slm_legacy_dashboard( + request: Request, + unit_id: str, + from_project: Optional[str] = None, + db: Session = Depends(get_db) +): """Legacy SLM control center dashboard for a specific unit""" + # Get project details if from_project is provided + project = None + if from_project: + from backend.models import Project + project = db.query(Project).filter_by(id=from_project).first() + return templates.TemplateResponse("slm_legacy_dashboard.html", { "request": request, - "unit_id": unit_id + "unit_id": unit_id, + "from_project": from_project, + "project": project }) diff --git a/backend/routers/projects.py b/backend/routers/projects.py index 5a72362..45ee26d 100644 --- a/backend/routers/projects.py +++ b/backend/routers/projects.py @@ -17,6 +17,7 @@ from datetime import datetime, timedelta from typing import Optional import uuid import json +import logging from backend.database import get_db from backend.models import ( @@ -31,6 +32,7 @@ from backend.models import ( router = APIRouter(prefix="/api/projects", tags=["projects"]) templates = Jinja2Templates(directory="templates") +logger = logging.getLogger(__name__) # ============================================================================ @@ -565,6 +567,183 @@ async def get_project_files( }) +@router.get("/{project_id}/ftp-browser", response_class=HTMLResponse) +async def get_ftp_browser( + project_id: str, + request: Request, + db: Session = Depends(get_db), +): + """ + Get FTP browser interface for downloading files from assigned SLMs. + Returns HTML partial with FTP browser. + """ + from backend.models import DataFile + + # Get all assignments for this project + assignments = db.query(UnitAssignment).filter( + and_( + UnitAssignment.project_id == project_id, + UnitAssignment.status == "active", + ) + ).all() + + # Enrich with unit and location details + units_data = [] + for assignment in assignments: + unit = db.query(RosterUnit).filter_by(id=assignment.unit_id).first() + location = db.query(MonitoringLocation).filter_by(id=assignment.location_id).first() + + # Only include SLM units + if unit and unit.device_type == "sound_level_meter": + units_data.append({ + "assignment": assignment, + "unit": unit, + "location": location, + }) + + return templates.TemplateResponse("partials/projects/ftp_browser.html", { + "request": request, + "project_id": project_id, + "units": units_data, + }) + + +@router.post("/{project_id}/ftp-download-to-server") +async def ftp_download_to_server( + project_id: str, + request: Request, + db: Session = Depends(get_db), +): + """ + Download a file from an SLM to the server via FTP. + Creates a DataFile record and stores the file in data/Projects/{project_id}/ + """ + import httpx + import os + import hashlib + from pathlib import Path + from backend.models import DataFile + + data = await request.json() + unit_id = data.get("unit_id") + remote_path = data.get("remote_path") + location_id = data.get("location_id") + + if not unit_id or not remote_path: + raise HTTPException(status_code=400, detail="Missing unit_id or remote_path") + + # Get or create active session for this location/unit + session = db.query(RecordingSession).filter( + and_( + RecordingSession.project_id == project_id, + RecordingSession.location_id == location_id, + RecordingSession.unit_id == unit_id, + RecordingSession.status.in_(["recording", "paused"]) + ) + ).first() + + # If no active session, create one + if not session: + session = RecordingSession( + id=str(uuid.uuid4()), + project_id=project_id, + location_id=location_id, + unit_id=unit_id, + status="completed", + started_at=datetime.utcnow(), + stopped_at=datetime.utcnow(), + notes="Auto-created for FTP download" + ) + db.add(session) + db.commit() + db.refresh(session) + + # Download file from SLMM + SLMM_BASE_URL = os.getenv("SLMM_BASE_URL", "http://localhost:8100") + + try: + async with httpx.AsyncClient(timeout=300.0) as client: + response = await client.post( + f"{SLMM_BASE_URL}/api/nl43/{unit_id}/ftp/download", + json={"remote_path": remote_path} + ) + + if not response.is_success: + raise HTTPException( + status_code=response.status_code, + detail=f"Failed to download from SLMM: {response.text}" + ) + + # Extract filename from remote_path + filename = os.path.basename(remote_path) + + # Determine file type from extension + ext = os.path.splitext(filename)[1].lower() + file_type_map = { + '.wav': 'audio', + '.mp3': 'audio', + '.csv': 'data', + '.txt': 'data', + '.log': 'log', + '.json': 'data', + } + file_type = file_type_map.get(ext, 'data') + + # Create directory structure: data/Projects/{project_id}/{session_id}/ + project_dir = Path(f"data/Projects/{project_id}/{session.id}") + project_dir.mkdir(parents=True, exist_ok=True) + + # Save file to disk + file_path = project_dir / filename + file_content = response.content + + with open(file_path, 'wb') as f: + f.write(file_content) + + # Calculate checksum + checksum = hashlib.sha256(file_content).hexdigest() + + # Create DataFile record + data_file = DataFile( + id=str(uuid.uuid4()), + session_id=session.id, + file_path=str(file_path.relative_to("data")), # Store relative to data/ + file_type=file_type, + file_size_bytes=len(file_content), + downloaded_at=datetime.utcnow(), + checksum=checksum, + file_metadata=json.dumps({ + "source": "ftp", + "remote_path": remote_path, + "unit_id": unit_id, + "location_id": location_id, + }) + ) + + db.add(data_file) + db.commit() + + return { + "success": True, + "message": f"Downloaded {filename} to server", + "file_id": data_file.id, + "file_path": str(file_path), + "file_size": len(file_content), + } + + except httpx.TimeoutException: + raise HTTPException( + status_code=504, + detail="Timeout downloading file from SLM" + ) + except Exception as e: + logger.error(f"Error downloading file to server: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to download file to server: {str(e)}" + ) + + # ============================================================================ # Project Types # ============================================================================ diff --git a/templates/partials/projects/ftp_browser.html b/templates/partials/projects/ftp_browser.html new file mode 100644 index 0000000..607af7a --- /dev/null +++ b/templates/partials/projects/ftp_browser.html @@ -0,0 +1,331 @@ + +
+

Download Files from SLMs

+ + {% if units %} +
+ {% for unit_item in units %} +
+ +
+
+

+ {{ unit_item.unit.id }} +

+ {% if unit_item.location %} + + @ {{ unit_item.location.name }} + + {% endif %} + + Checking... + +
+
+ + + +
+
+ + + +
+ {% endfor %} +
+ {% else %} +
+ + + +

No units assigned to this project

+
+ {% endif %} +
+ + diff --git a/templates/partials/projects/session_list.html b/templates/partials/projects/session_list.html index ef8cad5..0dd2739 100644 --- a/templates/partials/projects/session_list.html +++ b/templates/partials/projects/session_list.html @@ -33,7 +33,7 @@ {% if item.unit %}
Unit: - + {{ item.unit.id }}
diff --git a/templates/partials/projects/unit_list.html b/templates/partials/projects/unit_list.html index f8837e9..58c9453 100644 --- a/templates/partials/projects/unit_list.html +++ b/templates/partials/projects/unit_list.html @@ -7,7 +7,7 @@

- + {{ item.unit.id }}

@@ -73,7 +73,7 @@
- View Unit diff --git a/templates/projects/detail.html b/templates/projects/detail.html index efce1b2..a2eec82 100644 --- a/templates/projects/detail.html +++ b/templates/projects/detail.html @@ -180,9 +180,20 @@