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( async def get_ftp_browser(
project_id: str, project_id: str,
request: Request, request: Request,
location_id: Optional[str] = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Get FTP browser interface for downloading files from assigned SLMs. Get FTP browser interface for downloading files from assigned SLMs.
Returns HTML partial with FTP browser. Sound Monitoring projects only. 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 from backend.models import DataFile
project = db.query(Project).filter_by(id=project_id).first() project = db.query(Project).filter_by(id=project_id).first()
_require_module(project, "sound_monitoring", db) _require_module(project, "sound_monitoring", db)
# Get all assignments for this project (active = assigned_until IS NULL) # Active assignments for this project (active = assigned_until IS NULL),
assignments = db.query(UnitAssignment).filter( # optionally scoped to a single NRL/location.
q = db.query(UnitAssignment).filter(
and_( and_(
UnitAssignment.project_id == project_id, UnitAssignment.project_id == project_id,
UnitAssignment.assigned_until == None, 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 # Enrich with unit and location details
units_data = [] units_data = []
@@ -1882,21 +1890,26 @@ async def ftp_download_folder_to_server(
async def get_unified_files( async def get_unified_files(
project_id: str, project_id: str,
request: Request, request: Request,
location_id: Optional[str] = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" """
Get unified view of all files in this project. Get unified view of all files in this project.
Groups files by recording session with full metadata. Groups files by recording session with full metadata.
Returns HTML partial with hierarchical file listing. 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 backend.models import DataFile
from pathlib import Path from pathlib import Path
import json import json
# Get all sessions for this project # Sessions for this project (optionally scoped to one NRL/location)
sessions = db.query(MonitoringSession).filter_by( q = db.query(MonitoringSession).filter_by(project_id=project_id)
project_id=project_id if location_id:
).order_by(MonitoringSession.started_at.desc()).all() q = q.filter(MonitoringSession.location_id == location_id)
sessions = q.order_by(MonitoringSession.started_at.desc()).all()
sessions_data = [] sessions_data = []
for session in sessions: for session in sessions:
+22 -5
View File
@@ -357,6 +357,16 @@
<!-- Data Files Tab --> <!-- Data Files Tab -->
<div id="data-tab" class="tab-panel hidden"> <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="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<div class="flex items-center justify-between mb-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> <h2 class="text-xl font-semibold text-gray-900 dark:text-white">Data Files</h2>
@@ -369,6 +379,13 @@
</svg> </svg>
Upload Data Upload Data
</button> </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>
</div> </div>
@@ -408,11 +425,11 @@
</div> </div>
</div> </div>
<div id="data-files-list" <div id="unified-files"
hx-get="/api/projects/{{ project_id }}/nrl/{{ location_id }}/files" hx-get="/api/projects/{{ project_id }}/files-unified?location_id={{ location_id }}"
hx-trigger="load" hx-trigger="load, refresh from:#unified-files"
hx-swap="innerHTML"> 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> </div>
</div> </div>
@@ -715,7 +732,7 @@ function submitUpload() {
status.textContent = parts.join(' '); status.textContent = parts.join(' ');
status.className = 'text-sm text-green-600 dark:text-green-400'; status.className = 'text-sm text-green-600 dark:text-green-400';
input.value = ''; input.value = '';
htmx.trigger(document.getElementById('data-files-list'), 'load'); htmx.trigger(document.getElementById('unified-files'), 'refresh');
} else { } else {
status.textContent = `Error: ${data.detail || 'Upload failed'}`; status.textContent = `Error: ${data.detail || 'Upload failed'}`;
status.className = 'text-sm text-red-600 dark:text-red-400'; status.className = 'text-sm text-red-600 dark:text-red-400';