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:
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user