This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

View File

@@ -162,7 +162,15 @@
<div class="text-xs mt-1">or use the chooser above and click Upload</div>
</div>
<div id="uploadingIndicator" class="mt-2 text-sm text-neutral-500 hidden"><i class="fa-solid fa-spinner animate-spin mr-2"></i>Uploading…</div>
<div class="mt-2 flex items-center justify-between">
<div class="flex items-center gap-2 text-xs">
<span class="text-neutral-500">Live updates:</span>
<span id="docLiveBadge"></span>
</div>
<button id="reconnectDocWsBtn" type="button" class="text-xs px-2 py-1 rounded bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 text-neutral-700 dark:text-neutral-300 border border-neutral-300 dark:border-neutral-600">Reconnect</button>
</div>
<div id="uploadProgressList" class="space-y-2 mt-3"></div>
<div id="docEventFeed" class="space-y-2 mt-3" aria-live="polite"></div>
<div id="uploadedDocuments" class="mb-6">
<p class="text-neutral-500">Uploaded documents will appear here.</p>
</div>
@@ -520,6 +528,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Set up event handlers
setupEventHandlers();
// Live notifications UI for Generated tab
try { setupGeneratedTabNotifications(); } catch (_) {}
// Auto-refresh every 30 seconds
setInterval(function() {
@@ -742,14 +753,16 @@ function setupEventHandlers() {
// Auto-load uploads for restored file number and show one-time hint
try {
if ((saved || '').trim()) {
loadUploadedDocuments().then(() => {
try {
if (!sessionStorage.getItem('docs_auto_loaded_hint_shown')) {
showAlert(`Loaded uploads for file ${saved}`, 'info');
sessionStorage.setItem('docs_auto_loaded_hint_shown', '1');
}
} catch (_) {}
});
loadUploadedDocuments()
.then(() => backfillGeneratedForFile(saved))
.then(() => {
try {
if (!sessionStorage.getItem('docs_auto_loaded_hint_shown')) {
showAlert(`Loaded uploads for file ${saved}`, 'info');
sessionStorage.setItem('docs_auto_loaded_hint_shown', '1');
}
} catch (_) {}
});
}
} catch (_) {}
}
@@ -762,6 +775,205 @@ function setupEventHandlers() {
}
}
// Live notifications for file-specific events on the Generated tab
function setupGeneratedTabNotifications() {
const badgeHost = document.getElementById('docLiveBadge');
const feed = document.getElementById('docEventFeed');
const reconnectBtn = document.getElementById('reconnectDocWsBtn');
const uploadFileNoInput = document.getElementById('uploadFileNo');
let mgr = null;
let badge = null;
function attachBadge() {
if (!badgeHost || !window.notifications || !window.notifications.createConnectionBadge) return;
const created = window.notifications.createConnectionBadge();
badge = created;
badgeHost.innerHTML = '';
badgeHost.appendChild(created.element);
}
function onEvent(payload) {
if (feed && window.notifications && window.notifications.appendEvent) {
window.notifications.appendEvent(feed, {
fileNo: payload.fileNo,
status: payload.status,
message: payload.data && (payload.data.file_name || payload.data.filename) ? (payload.data.file_name || payload.data.filename) : (payload.message || null),
timestamp: payload.timestamp,
max: 50
});
}
try { updateUploadedBadgeFromEvent(payload); } catch (_) {}
try { upsertGeneratedFromEvent(payload); } catch (_) {}
}
function onState(state) {
try { if (badge && typeof badge.update === 'function') badge.update(state); } catch (_) {}
}
function connectFor(fileNo) {
if (!fileNo || !window.notifications || !window.notifications.connectFileNotifications) return;
if (mgr && typeof mgr.close === 'function') { try { mgr.close(); } catch (_) {} }
try { loadUploadedDocuments(); } catch (_) {}
try { backfillGeneratedForFile(fileNo); } catch (_) {}
mgr = window.notifications.connectFileNotifications({ fileNo, onEvent, onState });
if (!badge) attachBadge();
}
if (reconnectBtn) {
reconnectBtn.addEventListener('click', function(){ if (mgr && typeof mgr.reconnectNow === 'function') mgr.reconnectNow(); });
}
// Connect when a valid file number is present/changes
function maybeConnect() {
const fileNo = (uploadFileNoInput && uploadFileNoInput.value || '').trim();
if (fileNo) connectFor(fileNo);
}
if (uploadFileNoInput) {
uploadFileNoInput.addEventListener('change', maybeConnect);
uploadFileNoInput.addEventListener('blur', maybeConnect);
// initial
maybeConnect();
}
}
// ---------- Status badge helpers ----------
function getStatusBadgeHtml(status) {
const s = String(status || '').toLowerCase();
let cls = 'bg-neutral-100 text-neutral-700 border border-neutral-300';
if (s === 'processing') cls = 'bg-amber-100 text-amber-700 border border-amber-400';
else if (s === 'completed' || s === 'success' || s === 'uploaded' || s === 'ready') cls = 'bg-green-100 text-green-700 border border-green-400';
else if (s === 'failed' || s === 'error') cls = 'bg-red-100 text-red-700 border border-red-400';
const text = (s || 'unknown').toUpperCase();
return `<span class="doc-status-badge inline-block px-2 py-0.5 text-xs rounded ${cls}">${text}</span>`;
}
function updateBadgeElement(el, status) {
if (!el) return;
const wrapper = el.parentElement;
const html = getStatusBadgeHtml(status);
if (window.setSafeHTML) { window.setSafeHTML(wrapper, html); }
else { wrapper.innerHTML = html; }
}
// Update status badge for Uploaded table when matching document_id or filename
function updateUploadedBadgeFromEvent(payload) {
const data = payload && payload.data ? payload.data : {};
const docId = data.document_id != null ? String(data.document_id) : null;
const filename = data.filename || data.file_name || null;
if (!docId && !filename) return;
const container = document.getElementById('uploadedDocuments');
if (!container) return;
let row = null;
if (docId) {
row = container.querySelector(`tr[data-doc-id="${CSS.escape(String(docId))}"]`);
}
if (!row && filename) {
row = container.querySelector(`tr[data-filename="${CSS.escape(String(filename))}"]`);
}
if (!row) return;
const badge = row.querySelector('.doc-status-badge');
if (!badge) return;
const status = (data && data.action === 'upload') ? 'uploaded' : payload.status;
updateBadgeElement(badge, status);
}
// Ensure generated documents table exists
function ensureGeneratedTable() {
const container = document.getElementById('generatedDocuments');
if (!container) return null;
// If already a table, return tbody
let tbody = container.querySelector('#generatedDocsTableBody');
if (tbody) return tbody;
const html = `
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<thead>
<tr>
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">Size</th>
</tr>
</thead>
<tbody id="generatedDocsTableBody"></tbody>
</table>
`;
if (window.setSafeHTML) { window.setSafeHTML(container, html); }
else { container.innerHTML = html; }
return container.querySelector('#generatedDocsTableBody');
}
// Create or update a generated doc row
function upsertGeneratedFromEvent(payload) {
const status = String(payload && payload.status || '').toLowerCase();
if (!status) return;
const data = payload && payload.data ? payload.data : {};
const tbody = ensureGeneratedTable();
if (!tbody) return;
const fileNo = payload.fileNo || data.file_no || '';
const docId = data.document_id != null ? String(data.document_id) : null;
const filename = data.filename || data.file_name || (data.template_name ? `${data.template_name} (${fileNo})` : null);
const size = data.size != null ? Number(data.size) : null;
const keySelector = docId ? `tr[data-doc-id="${CSS.escape(docId)}"]` : (filename ? `tr[data-filename="${CSS.escape(filename)}"]` : null);
let row = keySelector ? tbody.querySelector(keySelector) : null;
if (!row) {
row = document.createElement('tr');
if (docId) row.setAttribute('data-doc-id', String(docId));
if (filename) row.setAttribute('data-filename', String(filename));
const nameCell = document.createElement('td');
nameCell.className = 'px-4 py-2';
nameCell.textContent = filename || '[Unknown]';
const statusCell = document.createElement('td');
statusCell.className = 'px-4 py-2';
if (window.setSafeHTML) { window.setSafeHTML(statusCell, getStatusBadgeHtml(status)); }
else { statusCell.innerHTML = getStatusBadgeHtml(status); }
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-2';
sizeCell.textContent = size != null ? `${Number(size).toLocaleString()} bytes` : '';
row.appendChild(nameCell);
row.appendChild(statusCell);
row.appendChild(sizeCell);
tbody.prepend(row);
} else {
const badge = row.querySelector('.doc-status-badge');
if (badge) updateBadgeElement(badge, status);
const sizeCell = row.children[2];
if (size != null && sizeCell) sizeCell.textContent = `${Number(size).toLocaleString()} bytes`;
}
}
// Backfill current generated documents for a file before live updates begin
async function backfillGeneratedForFile(fileNo) {
try {
if (!fileNo) return;
// 1) Status backfill for processing badge
try {
const statusResp = await window.http.wrappedFetch(`/api/documents/current-status/${encodeURIComponent(fileNo)}`);
if (statusResp && statusResp.ok) {
const st = await statusResp.json();
if (st && String(st.status || '').toLowerCase() === 'processing') {
// Surface a processing row in Generated section for immediate feedback
upsertGeneratedFromEvent({ fileNo, status: 'processing', data: (st.data || {}) });
}
}
} catch (_) {}
// 2) Seed existing generated docs from uploaded list
const resp = await window.http.wrappedFetch(`/api/documents/${encodeURIComponent(fileNo)}/uploaded`);
if (!resp.ok) { return; }
const docs = await resp.json();
const generated = Array.isArray(docs) ? docs.filter((d) => String(d.description || '').toLowerCase().includes('generated')) : [];
if (!generated.length) return;
for (const d of generated) {
try {
upsertGeneratedFromEvent({
fileNo,
status: 'completed',
data: { document_id: d.id, filename: d.filename, size: d.size }
});
} catch (_) {}
}
} catch (_) {}
}
function updateUploadControlsState() {
try {
const btn = document.getElementById('uploadBtn');
@@ -797,6 +1009,8 @@ function clearUploadFileNo() {
try { localStorage.removeItem('docs_last_upload_file_no'); } catch (_) {}
const container = document.getElementById('uploadedDocuments');
if (container) container.innerHTML = '<p class="text-neutral-500">No uploads found for this file.</p>';
const gen = document.getElementById('generatedDocuments');
if (gen) gen.innerHTML = '<p class="text-neutral-500">Generated documents will appear here...</p>';
} catch (_) {}
}
@@ -1395,11 +1609,12 @@ function displayUploadedDocuments(docs) {
return;
}
const rows = docs.map((d) => `
<tr>
<tr data-doc-id="${String(d.id || '')}" data-filename="${String(d.filename || '').replace(/"/g, '&quot;')}">
<td class="px-4 py-2">${d.id || ''}</td>
<td class="px-4 py-2">${d.filename || ''}</td>
<td class="px-4 py-2">${(d.type || '').split('/').pop()}</td>
<td class="px-4 py-2">${Number(d.size || 0).toLocaleString()} bytes</td>
<td class="px-4 py-2"><span class="doc-status-badge inline-block px-2 py-0.5 text-xs rounded bg-green-100 text-green-700 border border-green-400">UPLOADED</span></td>
<td class="px-4 py-2"><a href="/${d.path || ''}" target="_blank" class="text-primary-600 hover:underline">View</a></td>
<td class="px-4 py-2">
<button class="px-2 py-1 border border-cyan-600 text-cyan-600 rounded hover:bg-blue-100 mr-2" title="Edit description" onclick="openEditUploadModal(${JSON.stringify(String(d.id || ''))}, ${JSON.stringify(String(d.description || ''))})"><i class="fa-solid fa-pencil"></i></button>
@@ -1415,6 +1630,7 @@ function displayUploadedDocuments(docs) {
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Type</th>
<th class="px-4 py-2">Size</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">Link</th>
<th class="px-4 py-2">Actions</th>
</tr>