feat(reports): per-NRL Data Files tab reaches parity with the project-wide tab

The per-NRL Data Files tab now reuses the same FTP browser + unified-files partials as the project-wide tab, scoped to the one NRL: ftp-browser and files-unified take an optional location_id. nrl_detail.html drops the flat file_list view for 'Download Files from SLMs' (Browse Files -> Download & Save) plus the grouped 'Project Files' view (edit times / download-all / delete), keeping the NRL upload and adding a refresh button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 18:46:59 +00:00
parent 7716a4b51d
commit aa21c81c2e
2 changed files with 42 additions and 12 deletions
+20 -7
View File
@@ -1591,24 +1591,32 @@ async def get_sessions_calendar(
async def get_ftp_browser(
project_id: str,
request: Request,
location_id: Optional[str] = None,
db: Session = Depends(get_db),
):
"""
Get FTP browser interface for downloading files from assigned SLMs.
Returns HTML partial with FTP browser. Sound Monitoring projects only.
When `location_id` is given, scope to just the unit(s) assigned to that NRL
(used by the per-NRL Data Files tab, which mirrors the project-wide tab).
"""
from backend.models import DataFile
project = db.query(Project).filter_by(id=project_id).first()
_require_module(project, "sound_monitoring", db)
# Get all assignments for this project (active = assigned_until IS NULL)
assignments = db.query(UnitAssignment).filter(
# Active assignments for this project (active = assigned_until IS NULL),
# optionally scoped to a single NRL/location.
q = db.query(UnitAssignment).filter(
and_(
UnitAssignment.project_id == project_id,
UnitAssignment.assigned_until == None,
)
).all()
)
if location_id:
q = q.filter(UnitAssignment.location_id == location_id)
assignments = q.all()
# Enrich with unit and location details
units_data = []
@@ -1882,21 +1890,26 @@ async def ftp_download_folder_to_server(
async def get_unified_files(
project_id: str,
request: Request,
location_id: Optional[str] = None,
db: Session = Depends(get_db),
):
"""
Get unified view of all files in this project.
Groups files by recording session with full metadata.
Returns HTML partial with hierarchical file listing.
When `location_id` is given, scope to a single NRL/location (used by the
per-NRL Data Files tab so it mirrors the project-wide tab).
"""
from backend.models import DataFile
from pathlib import Path
import json
# Get all sessions for this project
sessions = db.query(MonitoringSession).filter_by(
project_id=project_id
).order_by(MonitoringSession.started_at.desc()).all()
# Sessions for this project (optionally scoped to one NRL/location)
q = db.query(MonitoringSession).filter_by(project_id=project_id)
if location_id:
q = q.filter(MonitoringSession.location_id == location_id)
sessions = q.order_by(MonitoringSession.started_at.desc()).all()
sessions_data = []
for session in sessions:
+22 -5
View File
@@ -357,6 +357,16 @@
<!-- Data Files Tab -->
<div id="data-tab" class="tab-panel hidden">
<!-- Download Files from SLMs (FTP browser, scoped to this NRL's assigned unit) -->
<div id="ftp-browser" class="mb-6"
hx-get="/api/projects/{{ project_id }}/ftp-browser?location_id={{ location_id }}"
hx-trigger="load"
hx-swap="innerHTML">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<div class="text-center py-8 text-gray-500">Loading FTP browser...</div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Data Files</h2>
@@ -369,6 +379,13 @@
</svg>
Upload Data
</button>
<button onclick="htmx.trigger('#unified-files', 'refresh')"
class="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Refresh
</button>
</div>
</div>
@@ -408,11 +425,11 @@
</div>
</div>
<div id="data-files-list"
hx-get="/api/projects/{{ project_id }}/nrl/{{ location_id }}/files"
hx-trigger="load"
<div id="unified-files"
hx-get="/api/projects/{{ project_id }}/files-unified?location_id={{ location_id }}"
hx-trigger="load, refresh from:#unified-files"
hx-swap="innerHTML">
<div class="text-center py-8 text-gray-500">Loading data files...</div>
<div class="text-center py-12 text-gray-500">Loading files...</div>
</div>
</div>
</div>
@@ -715,7 +732,7 @@ function submitUpload() {
status.textContent = parts.join(' ');
status.className = 'text-sm text-green-600 dark:text-green-400';
input.value = '';
htmx.trigger(document.getElementById('data-files-list'), 'load');
htmx.trigger(document.getElementById('unified-files'), 'refresh');
} else {
status.textContent = `Error: ${data.detail || 'Upload failed'}`;
status.className = 'text-sm text-red-600 dark:text-red-400';