diff --git a/apps/import/app.json b/apps/import/app.json new file mode 100644 index 000000000..853434054 --- /dev/null +++ b/apps/import/app.json @@ -0,0 +1,4 @@ +{ + "icon": "📥", + "label": "Import" +} diff --git a/convey/templates/import_detail.html b/apps/import/detail.html similarity index 92% rename from convey/templates/import_detail.html rename to apps/import/detail.html index d80978438..7a87fd6b3 100644 --- a/convey/templates/import_detail.html +++ b/apps/import/detail.html @@ -1,8 +1,11 @@ -{% extends 'base.html' %} -{% set title = 'Import Details' %} -{% set active = 'import' %} -{% block head %} - -{% endblock %} - -{% block body %} + + +
- ← Back to Imports - + ← Back to Imports +

Import: {{ timestamp }}

@@ -113,12 +115,12 @@
Loading...
- + - +
- +
Processing
@@ -178,7 +180,7 @@ tabs.forEach(tab => { const target = tab.dataset.target; tabs.forEach(t => t.classList.toggle('active', t.dataset.target === target)); contents.forEach(c => c.classList.toggle('active', c.id === target)); - + // Lazy load summary when tab is clicked if (target === 'summary' && !summaryLoaded) { loadSummary(); @@ -189,7 +191,7 @@ tabs.forEach(tab => { // Function to load summary function loadSummary() { summaryLoaded = true; - fetch('{{ url_for("import_view.import_summary_api", timestamp=timestamp) }}') + fetch('/app/import/api/{{ timestamp }}/summary') .then(r => r.json()) .then(data => { const summaryContent = document.getElementById('summaryContent'); @@ -207,7 +209,7 @@ function loadSummary() { // Format JSON with syntax highlighting function formatJson(obj) { if (!obj) return 'No data available'; - + const json = JSON.stringify(obj, null, 2); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'json-number'; @@ -271,7 +273,7 @@ const settingInput = document.getElementById('settingInput'); // Load facets for dropdown function loadFacets() { - fetch('{{ url_for("facets.facets_list") }}') + fetch('/facets/api/list') .then(r => r.json()) .then(facets => { const select = document.getElementById('facetSelect'); @@ -299,7 +301,7 @@ function watchImportProgress(importId) { progressTiming.textContent = ''; // Listen for importer events for this import - const cleanup = appEvents.listen('importer', msg => { + const cleanup = window.appEvents.listen('importer', msg => { if (msg.import_id !== importId) return; if (msg.event === 'started') { @@ -334,7 +336,7 @@ document.getElementById('rerunBtn').addEventListener('click', () => { btn.textContent = 'Starting...'; // Call the re-run endpoint - fetch('{{ url_for("import_view.import_rerun", timestamp=timestamp) }}', { + fetch('/app/import/api/{{ timestamp }}/rerun', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ facet: facet, setting: setting }) @@ -361,7 +363,7 @@ document.getElementById('rerunBtn').addEventListener('click', () => { }); // Load import details -fetch('{{ url_for("import_view.import_detail_api", timestamp=timestamp) }}') +fetch('/app/import/api/{{ timestamp }}') .then(r => r.json()) .then(data => { importData = data; // Store globally @@ -371,7 +373,7 @@ fetch('{{ url_for("import_view.import_detail_api", timestamp=timestamp) }}') const uploadTime = new Date(data.import_json.upload_datetime).toLocaleString(); const fileName = data.import_json.original_filename || 'Unknown'; const fileSize = formatFileSize(data.import_json.file_size); - + // Determine status based on task_id and imported_json let statusClass, statusText; if (data.imported_json) { @@ -385,18 +387,18 @@ fetch('{{ url_for("import_view.import_detail_api", timestamp=timestamp) }}') statusClass = 'pending'; statusText = 'Pending'; } - + metaDiv.innerHTML = ` - ${fileName} (${fileSize}) • + ${fileName} (${fileSize}) • Uploaded: ${uploadTime} ${statusText} `; } - + // Update overview tab const overviewContent = document.getElementById('overviewContent'); let overviewHtml = ''; - + if (data.import_json) { overviewHtml += `
Original File:
${data.import_json.original_filename || '-'}
`; overviewHtml += `
File Size:
${formatFileSize(data.import_json.file_size)}
`; @@ -414,31 +416,31 @@ fetch('{{ url_for("import_view.import_detail_api", timestamp=timestamp) }}') settingInput.value = data.import_json.setting || ''; } } - + if (data.imported_json) { overviewHtml += `
Processing Status:
Completed
`; overviewHtml += `
Target Day:
${data.imported_json.target_day || '-'}
`; overviewHtml += `
Files Created:
${data.imported_json.total_files_created || 0}
`; overviewHtml += `
Processing Time:
${new Date(data.imported_json.processing_completed).toLocaleString()}
`; - + // Show quick links if (data.imported_json.target_day) { const linksSection = document.getElementById('linksSection'); const quickLinks = document.getElementById('quickLinks'); linksSection.style.display = 'block'; - + let linksHtml = ''; linksHtml += ``; linksHtml += ``; quickLinks.innerHTML = linksHtml; } - + // Show created files if (data.imported_json.all_created_files && data.imported_json.all_created_files.length > 0) { const filesList = document.getElementById('filesList'); const filesListContent = document.getElementById('filesListContent'); filesList.style.display = 'block'; - + let filesHtml = ''; data.imported_json.all_created_files.forEach(file => { const basename = file.split('/').pop(); @@ -449,26 +451,26 @@ fetch('{{ url_for("import_view.import_detail_api", timestamp=timestamp) }}') } else { overviewHtml += `
Processing Status:
Pending
`; } - + overviewContent.innerHTML = overviewHtml; - + // Update JSON tabs document.getElementById('importJsonContent').innerHTML = formatJson(data.import_json); document.getElementById('importedJsonContent').innerHTML = data.imported_json ? formatJson(data.imported_json) : 'Processing not yet completed'; - + // Show summary tab if summary exists if (data.has_summary) { const summaryTab = document.getElementById('summaryTab'); summaryTab.style.display = 'block'; } - + // Show re-run section if import is completed or failed and file exists if (data.import_json && data.import_json.file_path) { // Show re-run section for completed or failed imports if (data.imported_json || (data.import_json.task_id && !data.imported_json)) { const rerunSection = document.getElementById('rerunSection'); rerunSection.style.display = 'block'; - + // Load facets for the dropdown loadFacets(); } @@ -482,4 +484,46 @@ fetch('{{ url_for("import_view.import_detail_api", timestamp=timestamp) }}') document.getElementById('importedJsonContent').innerHTML = 'Error loading data'; }); -{% endblock %} + + + + diff --git a/convey/views/import.py b/apps/import/routes.py similarity index 95% rename from convey/views/import.py rename to apps/import/routes.py index 608c3b51b..24938f8d0 100644 --- a/convey/views/import.py +++ b/apps/import/routes.py @@ -22,17 +22,22 @@ from think.importer_utils import ( write_import_metadata, ) -from .. import state +from convey import state -bp = Blueprint("import_view", __name__, template_folder="../templates") +import_bp = Blueprint( + "import", + __name__, + url_prefix="/app/import", +) -@bp.route("/import") -def import_page() -> str: - return render_template("import.html", active="import") +@import_bp.route("/") +def index() -> str: + """Render the import page.""" + return render_template("app.html", app="import") -@bp.route("/import/api/save", methods=["POST"]) +@import_bp.route("/api/save", methods=["POST"]) def import_save() -> Any: from datetime import datetime @@ -158,7 +163,7 @@ def import_save() -> Any: ) -@bp.route("/import/api/facet", methods=["POST"]) +@import_bp.route("/api/facet", methods=["POST"]) def import_update_metadata() -> Any: """Update stored metadata (facet/setting) for a saved import.""" data = request.get_json(force=True) @@ -196,7 +201,7 @@ def import_update_metadata() -> Any: ) -@bp.route("/import/api/list") +@import_bp.route("/api/list") def import_list() -> Any: """Get list of all imports with their metadata.""" # Get all import timestamps using utility function @@ -243,17 +248,17 @@ def import_list() -> Any: return jsonify(imports) -@bp.route("/import/") +@import_bp.route("/") def import_detail(timestamp: str) -> str: """Show detailed view of a specific import.""" import_dir = Path(state.journal_root) / "imports" / timestamp if not import_dir.exists(): return render_template("error.html", error="Import not found"), 404 - return render_template("import_detail.html", timestamp=timestamp, active="import") + return render_template("apps/import/detail.html", timestamp=timestamp, active="import") -@bp.route("/import/api/") +@import_bp.route("/api/") def import_detail_api(timestamp: str) -> Any: """Get detailed data for a specific import.""" try: @@ -267,7 +272,7 @@ def import_detail_api(timestamp: str) -> Any: return jsonify({"error": "Import not found"}), 404 -@bp.route("/import/api//summary") +@import_bp.route("/api//summary") def import_summary_api(timestamp: str) -> Any: """Get the summary HTML for a specific import.""" import_dir = Path(state.journal_root) / "imports" / timestamp @@ -305,7 +310,7 @@ def import_summary_api(timestamp: str) -> Any: ) -@bp.route("/import/api/start", methods=["POST"]) +@import_bp.route("/api/start", methods=["POST"]) def import_start() -> Any: data = request.get_json(force=True) path = data.get("path") @@ -403,7 +408,7 @@ def import_start() -> Any: return jsonify({"status": "ok", "task_id": task_id}) -@bp.route("/import/api//rerun", methods=["POST"]) +@import_bp.route("/api//rerun", methods=["POST"]) def import_rerun(timestamp: str) -> Any: """Re-run an import with optionally updated facet.""" journal_root = Path(state.journal_root) diff --git a/convey/templates/import.html b/apps/import/workspace.html similarity index 96% rename from convey/templates/import.html rename to apps/import/workspace.html index 68619bd92..71d7e874f 100644 --- a/convey/templates/import.html +++ b/apps/import/workspace.html @@ -1,7 +1,3 @@ -{% extends 'base.html' %} -{% set title = 'Import' %} -{% set active = 'import' %} -{% block head %} -{% endblock %} -{% block body %}
Upload File
@@ -63,7 +57,7 @@ button:disabled { background:#ccc; color:#666; cursor:not-allowed; opacity:0.6;
- +

Import History

@@ -127,11 +121,11 @@ document.getElementById('importForm').onsubmit=e=>{ e.preventDefault(); const submitBtn = document.getElementById('submitBtn'); const originalText = submitBtn.textContent; - + // Disable button and show processing state submitBtn.disabled = true; submitBtn.innerHTML = 'Processing'; - + const fd=new FormData(); if(activeTab==='media'){ if(currentFile) fd.append('file', currentFile); @@ -143,13 +137,13 @@ document.getElementById('importForm').onsubmit=e=>{ if(facet) fd.append('facet', facet); const settingValue = settingInput.value.trim(); if(settingValue) fd.append('setting', settingValue); - fetch('{{ url_for('import_view.import_save') }}', {method:'POST', body:fd}) + fetch('/app/import/api/save', {method:'POST', body:fd}) .then(async r=>{ const d=await r.json(); // Re-enable button submitBtn.disabled = false; submitBtn.textContent = originalText; - + if(!r.ok){ if(window.showError)showError(d.error||'Upload failed'); return; @@ -209,7 +203,7 @@ function capitalizeStage(stage) { } function loadImports(){ - fetch('{{ url_for('import_view.import_list') }}').then(r=>r.json()).then(imports=>{ + fetch('/app/import/api/list').then(r=>r.json()).then(imports=>{ const container = document.getElementById('importListContent'); if (!imports || imports.length === 0) { @@ -254,7 +248,7 @@ function loadImports(){ statusText = 'Pending'; } - html += ``; + html += ``; html += `${importedAt}`; html += `${imp.original_filename || 'Unknown'}`; html += `${formatFileSize(imp.file_size || 0)}`; @@ -342,7 +336,7 @@ startBtn.addEventListener('click', async () => { startBtn.innerHTML = 'Starting'; try { - const response = await fetch('{{ url_for('import_view.import_update_metadata') }}', { + const response = await fetch('/app/import/api/facet', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path, facet, setting }) @@ -354,7 +348,7 @@ startBtn.addEventListener('click', async () => { } // Start the import task - const startResponse = await fetch('{{ url_for('import_view.import_start') }}', { + const startResponse = await fetch('/app/import/api/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path, timestamp: ts }) @@ -387,7 +381,7 @@ window.onclick=e=>{if(e.target==document.getElementById('detectModal'))document. // Load facets for dropdown function loadFacets(){ - fetch('{{ url_for('facets.facets_list') }}').then(r=>r.json()).then(facets=>{ + fetch('/facets/api/list').then(r=>r.json()).then(facets=>{ const select=document.getElementById('facetSelect'); const current=select.value; // Preserve current selection select.innerHTML=''; @@ -419,4 +413,3 @@ loadImports(); loadFacets(); setTimeout(checkUrlFragment, 100); // Wait for facets to load -{% endblock %} diff --git a/convey/__init__.py b/convey/__init__.py index c4b83f431..939310a59 100644 --- a/convey/__init__.py +++ b/convey/__init__.py @@ -33,9 +33,6 @@ from .views import search as search_view # Old task system removed - now using Callosum for task execution -import_page_view = import_module(".import", "convey.views") - - logger = logging.getLogger(__name__) @@ -238,7 +235,6 @@ send_message = chat_view.send_message chat_history = chat_view.chat_history clear_history = chat_view.clear_history search_page = search_view.search_page -import_page = import_page_view.import_page calendar_days = calendar_view.calendar_days calendar_stats = calendar_view.calendar_stats calendar_transcript_page = calendar_view.calendar_transcript_page @@ -265,7 +261,6 @@ __all__ = [ "chat_history", "clear_history", "search_page", - "import_page", "calendar_days", "calendar_stats", "calendar_transcript_page", diff --git a/convey/templates/base.html b/convey/templates/base.html index 2e2cd308d..b9aa4b510 100644 --- a/convey/templates/base.html +++ b/convey/templates/base.html @@ -17,7 +17,6 @@ 💬 🚀 🔍 - 📥 🔒