From 24c5a0263e6121478e3f2393f4f2e871dfe7480f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:45:20 +0000 Subject: [PATCH 1/3] Initial plan From da865b5f7aa5f51b69432c060d04018381970a23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:46:41 +0000 Subject: [PATCH 2/3] fix(search): correct file detail page URL in search results Both the /search page and the inline search in /files were generating links to /files/{id} which returns 404. The correct route is /files/{id}/detail. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- frontend/templates/files.html | 2 +- frontend/templates/search.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/templates/files.html b/frontend/templates/files.html index d4f13beb..32713539 100644 --- a/frontend/templates/files.html +++ b/frontend/templates/files.html @@ -1012,7 +1012,7 @@ ${snippet ? `
…${snippet}…
` : ''}
- +
diff --git a/frontend/templates/search.html b/frontend/templates/search.html index 0a9f771b..6f4807a6 100644 --- a/frontend/templates/search.html +++ b/frontend/templates/search.html @@ -196,7 +196,7 @@ var tags = Array.isArray(hit.tags) ? hit.tags : (hit.tags ? [hit.tags] : []); var docType = hit.document_type || ''; var sender = hit.sender || hit.absender || ''; - var fileUrl = '/files/' + hit.file_id; + var fileUrl = '/files/' + hit.file_id + '/detail'; // Build badges var badges = ''; From c540dd4f5209644b4d450c006c78e85c4c5d422e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:02:25 +0000 Subject: [PATCH 3/3] feat(views): add document-centric view at /files/{id} - Revert search.html and files.html links back to /files/{id} - Add GET /files/{file_id} route (file_view_page) with workdir path- containment guards (os.path.commonpath) to prevent traversal - Create file_view.html: document-centric page showing AI metadata, inline PDF preview, extracted OCR text, download actions, file info, status pill, and link to /files/{id}/detail for process pipeline view - Existing /files/{id}/detail route is unchanged Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/views/files.py | 82 ++++++ frontend/templates/file_view.html | 415 ++++++++++++++++++++++++++++++ frontend/templates/files.html | 2 +- frontend/templates/search.html | 2 +- 4 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 frontend/templates/file_view.html diff --git a/app/views/files.py b/app/views/files.py index d2395dc7..6feac24d 100644 --- a/app/views/files.py +++ b/app/views/files.py @@ -128,6 +128,88 @@ def files_page( ) +@router.get("/files/{file_id}") +@require_login +def file_view_page(request: Request, file_id: int, db: Session = Depends(get_db)): + """ + Return the document view page — document-centric view with metadata, preview, and extracted text. + Process-oriented details are available via /files/{file_id}/detail. + """ + try: + import json + import os + + from app.models import FileRecord + + file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first() + + if not file_record: + return templates.TemplateResponse( + "file_view.html", + {"request": request, "file": None, "error": f"File with ID {file_id} not found"}, + ) + + from app.config import settings + + # Resolve the expected base directory to guard against path traversal in DB values. + workdir = os.path.realpath(settings.workdir) + + def _safe_exists(path: str | None) -> bool: + """Return True only when *path* exists and resides within workdir.""" + if not path: + return False + resolved = os.path.realpath(path) + try: + common = os.path.commonpath([resolved, workdir]) + except ValueError: + return False + return common == workdir and os.path.exists(resolved) + + # Check whether the backing files exist on disk + original_file_exists = _safe_exists(file_record.original_file_path) + processed_file_exists = _safe_exists(file_record.processed_file_path) + + # Load AI metadata — JSON sidecar file first, then DB column + gpt_metadata = None + if file_record.processed_file_path: + metadata_path = os.path.splitext(os.path.realpath(file_record.processed_file_path))[0] + ".json" + if _safe_exists(metadata_path): + try: + with open(metadata_path, "r", encoding="utf-8") as f: + gpt_metadata = json.load(f) + except Exception as e: + logger.warning(f"Failed to load metadata sidecar for file {file_id}: {e}") + + if gpt_metadata is None and file_record.ai_metadata: + try: + gpt_metadata = json.loads(file_record.ai_metadata) + except Exception as e: + logger.warning(f"Failed to parse ai_metadata for file {file_id}: {e}") + + # Quick processing status (no logs needed) + try: + from app.utils.step_manager import get_step_summary as _get_step_summary + + step_summary = _get_step_summary(db, file_id) + except Exception: + step_summary = None + + return templates.TemplateResponse( + "file_view.html", + { + "request": request, + "file": file_record, + "gpt_metadata": gpt_metadata, + "original_file_exists": original_file_exists, + "processed_file_exists": processed_file_exists, + "step_summary": step_summary, + }, + ) + except Exception as e: + logger.error(f"Error retrieving file view {file_id}: {str(e)}") + return templates.TemplateResponse("file_view.html", {"request": request, "file": None, "error": str(e)}) + + @router.get("/files/{file_id}/detail") @require_login def file_detail_page(request: Request, file_id: int, db: Session = Depends(get_db)): diff --git a/frontend/templates/file_view.html b/frontend/templates/file_view.html new file mode 100644 index 00000000..7bb40047 --- /dev/null +++ b/frontend/templates/file_view.html @@ -0,0 +1,415 @@ +{% extends "base.html" %} +{% block title %}{{ file.document_title or file.original_filename or 'Document' }} - DocuElevate{% endblock %} + +{% block head_extra %} + + +{% endblock %} + +{% block content %} +
+ + {% if error %} +
Error: {{ error }}
+ {% elif file %} + + + + Back to Files + + +
+
+
+ {% if gpt_metadata and gpt_metadata.filename %} + {{ gpt_metadata.filename | replace('.pdf','') | replace('_',' ') }} + {% elif file.document_title %} + {{ file.document_title }} + {% else %} + {{ file.original_filename or '(untitled)' }} + {% endif %} +
+
{{ file.original_filename }}
+
+ +
+ + {% if file.is_duplicate %} + Duplicate + {% elif step_summary %} + {% set main_completed = step_summary.main.success + step_summary.main.skipped %} + {% if step_summary.main.failure > 0 or step_summary.uploads.failure > 0 %} + Failed + {% elif step_summary.main["in_progress"] > 0 or step_summary.uploads["in_progress"] > 0 %} + Processing + {% elif step_summary.total_main_steps > 0 and main_completed == step_summary.total_main_steps and step_summary.main.failure == 0 %} + Completed + {% else %} + Pending + {% endif %} + {% endif %} + + + + Processing Details + +
+
+ + +
+ + +
+ {% if gpt_metadata %} +
+
Document Metadata
+ + {% if gpt_metadata.document_type %} +
+ Type + {{ gpt_metadata.document_type }} +
+ {% endif %} + + {% if gpt_metadata.date %} +
+ Date + {{ gpt_metadata.date }} +
+ {% endif %} + + {% if gpt_metadata.absender %} +
+ Sender + {{ gpt_metadata.absender }} +
+ {% endif %} + + {% if gpt_metadata.empfaenger %} +
+ Recipient + {{ gpt_metadata.empfaenger }} +
+ {% endif %} + + {% if gpt_metadata.betrag %} +
+ Amount + {{ gpt_metadata.betrag }} +
+ {% endif %} + + {% if gpt_metadata.kontonummer %} +
+ Account + {{ gpt_metadata.kontonummer }} +
+ {% endif %} + + {% if gpt_metadata.tags %} +
+ Tags + + {% if gpt_metadata.tags is string %} + {% for t in gpt_metadata.tags.split(',') %} + {{ t.strip() }} + {% endfor %} + {% else %} + {% for t in gpt_metadata.tags %} + {{ t }} + {% endfor %} + {% endif %} + +
+ {% endif %} + + {% if gpt_metadata.filename %} +
+ Suggested name + {{ gpt_metadata.filename }} +
+ {% endif %} +
+ {% endif %} + + +
+
File Info
+
+ Uploaded + {{ file.created_at.strftime('%Y-%m-%d %H:%M') if file.created_at else 'N/A' }} +
+
+ Size + {{ (file.file_size / 1024) | round(1) }} KB +
+
+ MIME type + {{ file.mime_type or 'N/A' }} +
+
+ ID + {{ file.id }} +
+
+ Hash + {{ file.filehash[:32] }}… +
+
+ + +
+
Actions
+
+ {% if processed_file_exists %} + + Download (processed) + + {% endif %} + {% if original_file_exists %} + + Download (original) + + {% endif %} + {% if processed_file_exists %} + + Open processed + + {% elif original_file_exists %} + + Open original + + {% endif %} +
+
+
+ + +
+
+
Preview
+ {% if processed_file_exists %} + +
Processed version
+ {% elif original_file_exists %} + +
Original version
+ {% else %} +
+ +

No file available for preview

+
+ {% endif %} +
+
+
+ + + {% if file.ocr_text %} +
+
+
Extracted Text
+
+ + +
+
+ +
+ {% elif processed_file_exists or original_file_exists %} +
+
+
Extracted Text
+ +
+
+
+ {% endif %} + + {% else %} + +
Document not found.
+ {% endif %} + +
+ + +{% endblock %} diff --git a/frontend/templates/files.html b/frontend/templates/files.html index 32713539..d4f13beb 100644 --- a/frontend/templates/files.html +++ b/frontend/templates/files.html @@ -1012,7 +1012,7 @@ ${snippet ? `
…${snippet}…
` : ''}
- +
diff --git a/frontend/templates/search.html b/frontend/templates/search.html index 6f4807a6..0a9f771b 100644 --- a/frontend/templates/search.html +++ b/frontend/templates/search.html @@ -196,7 +196,7 @@ var tags = Array.isArray(hit.tags) ? hit.tags : (hit.tags ? [hit.tags] : []); var docType = hit.document_type || ''; var sender = hit.sender || hit.absender || ''; - var fileUrl = '/files/' + hit.file_id + '/detail'; + var fileUrl = '/files/' + hit.file_id; // Build badges var badges = '';