Harden export preview handling
This commit is contained in:
@@ -7,6 +7,14 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## Sprint 38 Export Center preview hardening (2026-06-17)
|
||||
|
||||
- Prevented HTML project report artifacts from being offered through the JSON preview path in the frontend Export Center.
|
||||
- Added a clear backend `EXPORT_CONTENT_UNSUPPORTED` response when `/api/v1/exports/{export_id}/content` is called for HTML report artifacts.
|
||||
- Extracted export JSON preview rendering into `frontend/src/components/exports/ExportPreview.tsx`.
|
||||
- Added regression coverage for HTML report content-preview rejection.
|
||||
- No API routes, migrations, product features, provider fetching or AI behavior were introduced.
|
||||
|
||||
## Sprint 37 Tower PostgreSQL collation maintenance (2026-06-17)
|
||||
|
||||
- Created a live Tower database backup before collation maintenance: `backups/geointel-before-collation-refresh-20260617-065707.dump`.
|
||||
|
||||
@@ -192,6 +192,13 @@ class ExportService:
|
||||
if not export:
|
||||
raise AppError(code="EXPORT_NOT_FOUND", message="Export not found", status_code=404)
|
||||
path = ExportService.get_export_download_path(db, export_id)
|
||||
if export.export_type == "project_report_html" or path.suffix.lower() in {".html", ".htm"}:
|
||||
raise AppError(
|
||||
code="EXPORT_CONTENT_UNSUPPORTED",
|
||||
message="Export content preview is only available for JSON and GeoJSON artifacts. Download HTML report artifacts instead.",
|
||||
details={"export_type": export.export_type},
|
||||
status_code=415,
|
||||
)
|
||||
try:
|
||||
content = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
|
||||
@@ -259,6 +259,28 @@ def test_export_content_reads_persisted_artifact(tmp_path) -> None:
|
||||
assert response.content == {"hello": "world"}
|
||||
|
||||
|
||||
def test_export_content_rejects_html_report_preview(tmp_path) -> None:
|
||||
export_id = uuid4()
|
||||
export_path = tmp_path / "report.html"
|
||||
export_path.write_text("<!doctype html><html><body>report</body></html>", encoding="utf-8")
|
||||
export = Export(
|
||||
id=export_id,
|
||||
project_id=uuid4(),
|
||||
export_type="project_report_html",
|
||||
storage_path=str(export_path),
|
||||
metadata_json={"format": "html"},
|
||||
)
|
||||
db = FakeSession({(Export, export_id): export})
|
||||
|
||||
try:
|
||||
ExportService.get_export_content(db, export_id)
|
||||
except AppError as exc:
|
||||
assert exc.code == "EXPORT_CONTENT_UNSUPPORTED"
|
||||
assert exc.status_code == 415
|
||||
else:
|
||||
raise AssertionError("HTML report artifacts must be download-only through the content preview API")
|
||||
|
||||
|
||||
def test_export_download_path_rejects_missing_artifact(tmp_path) -> None:
|
||||
export_id = uuid4()
|
||||
export = Export(
|
||||
|
||||
+13
-5
@@ -1010,14 +1010,22 @@ Returns one persisted export record.
|
||||
### GET `/api/v1/exports/{export_id}/content`
|
||||
|
||||
Returns the stored JSON artifact content through the standard API envelope.
|
||||
HTML report artifacts are intentionally download-only through `/download`.
|
||||
Calling `/content` for `project_report_html` returns:
|
||||
|
||||
```text
|
||||
code: EXPORT_CONTENT_UNSUPPORTED
|
||||
message: Export content preview is only available for JSON and GeoJSON artifacts. Download HTML report artifacts instead.
|
||||
```
|
||||
|
||||
### GET `/api/v1/exports/{export_id}/download`
|
||||
|
||||
Downloads the stored JSON/GeoJSON export artifact as a raw file response with
|
||||
`application/json` content type and a `Content-Disposition` attachment
|
||||
filename. This endpoint intentionally does not use the JSON envelope because
|
||||
it is a browser/file-download path; callers that need canonical API JSON should
|
||||
use `/content`.
|
||||
Downloads the stored JSON/GeoJSON/HTML export artifact as a raw file response
|
||||
with a `Content-Disposition` attachment filename. JSON and GeoJSON artifacts
|
||||
use `application/json`; HTML report artifacts use `text/html`. This endpoint
|
||||
intentionally does not use the JSON envelope because it is a browser/file-download
|
||||
path; callers that need canonical API JSON should use `/content` for JSON/GeoJSON
|
||||
artifacts.
|
||||
|
||||
### POST `/api/v1/exports/yolo`
|
||||
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
## Sprint 38 Export Center preview hardening (2026-06-17)
|
||||
|
||||
Changed:
|
||||
- Hardened the export content preview path so HTML report artifacts return `EXPORT_CONTENT_UNSUPPORTED` instead of a generic JSON parse failure.
|
||||
- Updated the frontend Export Center to offer JSON preview only for JSON/GeoJSON artifacts.
|
||||
- HTML project report artifacts now display as download-only in the export list.
|
||||
- Extracted export preview rendering from `frontend/src/App.tsx` into `frontend/src/components/exports/ExportPreview.tsx`.
|
||||
- Updated API/frontend docs, changelog and TODO status.
|
||||
|
||||
Validation:
|
||||
- `cd backend && python -m pytest tests/test_sprint17_export_foundation.py -q` passed: 10 tests.
|
||||
- `cd frontend && npm run typecheck` passed.
|
||||
|
||||
Open:
|
||||
- Full release readiness and Tower redeploy/browser smoke are still recommended before treating this pass as deployed.
|
||||
|
||||
Limitations:
|
||||
- This pass does not add new export formats, PDF reports, provider fetching, AI inference or migrations.
|
||||
|
||||
Next recommended pass:
|
||||
- Run the full readiness gate, frontend build and Tower deploy smoke; then continue with shared workbench orchestration decomposition or export cleanup/history filtering.
|
||||
|
||||
## Sprint 37 Tower PostgreSQL collation maintenance (2026-06-17)
|
||||
|
||||
Changed:
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@ This file now starts with the current implementation status. Older preparation/b
|
||||
- [x] QA/QC results and map workspace component decomposition.
|
||||
- [x] Docker Compose port/storage/database configuration via `.env` defaults for Unraid.
|
||||
- [x] Single-container `geointel` Unraid compose/template runtime.
|
||||
- [ ] Further frontend component decomposition for export preview and shared workbench orchestration.
|
||||
- [x] Export preview component decomposition and HTML report download-only UX hardening.
|
||||
- [ ] Further frontend shared workbench orchestration decomposition.
|
||||
|
||||
## Sprint 8 status
|
||||
|
||||
|
||||
@@ -208,6 +208,8 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
||||
|
||||
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
||||
- The MapLibre chunk is intentionally larger than generic app chunks because it contains the GIS map runtime; the Vite warning threshold is set to keep this known vendor dependency visible without warning on every release build.
|
||||
- Export preview rendering lives in `src/components/exports/ExportPreview.tsx`.
|
||||
- The Export Center only offers JSON preview for JSON/GeoJSON artifacts; HTML project reports are shown as download-only artifacts.
|
||||
|
||||
## Raster dependency visibility
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
|
||||
import { DatasetPanel } from './components/datasets/DatasetPanel'
|
||||
import { DetectionLab } from './components/detection/DetectionLab'
|
||||
import { ExportCenter } from './components/exports/ExportCenter'
|
||||
import { ExportPreview } from './components/exports/ExportPreview'
|
||||
import { MapWorkspace } from './components/map/MapWorkspace'
|
||||
import { AreaPanel } from './components/project/AreaPanel'
|
||||
import { ProjectPanel } from './components/project/ProjectPanel'
|
||||
@@ -740,12 +741,7 @@ function App(): JSX.Element {
|
||||
onPreviewContent={previewExportContent}
|
||||
onDownload={downloadExportArtifact}
|
||||
/>
|
||||
{exportPreview ? (
|
||||
<section>
|
||||
<h2>Export Preview</h2>
|
||||
<pre className="job-result">{JSON.stringify(exportPreview, null, 2)}</pre>
|
||||
</section>
|
||||
) : null}
|
||||
<ExportPreview content={exportPreview} />
|
||||
|
||||
<DatasetPanel
|
||||
selectedProjectId={selectedProjectId}
|
||||
|
||||
@@ -24,6 +24,16 @@ function isVectorDatasetType(datasetType: string): boolean {
|
||||
return datasetType === 'vector' || datasetType === 'geojson'
|
||||
}
|
||||
|
||||
function canPreviewJson(item: ExportRead): boolean {
|
||||
if (item.export_type === 'project_report_html') {
|
||||
return false
|
||||
}
|
||||
if (item.metadata_json?.format === 'html') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function ExportCenter({
|
||||
selectedProjectId,
|
||||
selectedDataset,
|
||||
@@ -80,9 +90,13 @@ export function ExportCenter({
|
||||
<div>status: {item.status}</div>
|
||||
<div>path: {item.storage_path}</div>
|
||||
<div>export id: {item.id}</div>
|
||||
{canPreviewJson(item) ? (
|
||||
<button type="button" onClick={() => onPreviewContent(item.id)}>
|
||||
Preview JSON content
|
||||
</button>
|
||||
) : (
|
||||
<div>Preview: download-only HTML artifact</div>
|
||||
)}
|
||||
<button type="button" onClick={() => onDownload(item.id)}>
|
||||
Download artifact
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
interface ExportPreviewProps {
|
||||
content: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export function ExportPreview({ content }: ExportPreviewProps): JSX.Element | null {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Export Preview</h2>
|
||||
<pre className="job-result">{JSON.stringify(content, null, 2)}</pre>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user