// Carbone Word Template Manager - Frontend // Use same host as the page, just different port const CARBONE_API = window.location.protocol + '//' + window.location.hostname + ':8771/api/carbone'; // ============================================================ // NAVIGATION // ============================================================ // Load carbone templates when the templates page is shown const carboneNavObserver = new MutationObserver(() => { if (document.getElementById('page-templates') && document.getElementById('page-templates').classList.contains('active')) { loadCarboneTemplates(); } }); document.addEventListener('DOMContentLoaded', () => { const templatesPage = document.getElementById('page-templates'); if (templatesPage) carboneNavObserver.observe(templatesPage, { attributes: true, attributeFilter: ['class'] }); }); // ============================================================ // DOWNLOAD MASTER TEMPLATE // ============================================================ function downloadMasterTemplate() { window.open(CARBONE_API + '/master-template', '_blank'); } // ============================================================ // UPLOAD CUSTOM TEMPLATE // ============================================================ const carboneUploadZone = document.getElementById('carbone-upload-zone'); if (carboneUploadZone) { carboneUploadZone.addEventListener('dragover', e => { e.preventDefault(); carboneUploadZone.classList.add('dragover'); }); carboneUploadZone.addEventListener('dragleave', () => carboneUploadZone.classList.remove('dragover')); carboneUploadZone.addEventListener('drop', e => { e.preventDefault(); carboneUploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length) uploadCarboneTemplate(e.dataTransfer.files[0]); }); } async function uploadCarboneTemplate(file) { if (!file) return; const name = document.getElementById('carbone-template-name').value || file.name.replace(/\.[^.]+$/, ''); const status = document.getElementById('carbone-upload-status'); status.innerHTML = '
No custom templates uploaded yet. Download the master template, customize it in Word, and upload it here.
'; return; } list.innerHTML = data.templates.map(t => `Error loading templates: ' + e.message + '
'; } } // ============================================================ // RENDER SAMPLE CV // ============================================================ async function renderSampleCV(templateId) { toast('Generating sample CV PDF...'); // Get first candidate for sample data try { const candidatesResp = await fetch('/api/candidates?limit=1'); const candidatesData = await candidatesResp.json(); let cvData = null; if (candidatesData.candidates && candidatesData.candidates.length > 0) { const fullResp = await fetch('/api/candidates/' + candidatesData.candidates[0].id); cvData = await fullResp.json(); } if (!cvData) { toast('Upload a CV first to generate a sample', 'error'); return; } const resp = await fetch(CARBONE_API + '/templates/' + templateId + '/render', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ candidateData: cvData }) }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.error || 'Render failed'); } const blob = await resp.blob(); const url = URL.createObjectURL(blob); window.open(url, '_blank'); toast('Sample CV generated'); } catch (e) { toast('Render failed: ' + e.message, 'error'); } } // ============================================================ // DELETE TEMPLATE // ============================================================ async function deleteCarboneTemplate(templateId) { if (!confirm('Delete this template?')) return; try { await fetch(CARBONE_API + '/templates/' + templateId, { method: 'DELETE' }); toast('Template deleted'); loadCarboneTemplates(); } catch (e) { toast('Delete failed: ' + e.message, 'error'); } } // ============================================================ // TAGS HELP - View in app // ============================================================ async function viewTagsHelp() { try { const resp = await fetch(CARBONE_API + '/help'); const data = await resp.json(); let html = '${section.note}
`; html += '| Tag | Description |
|---|---|
| ${escapeHtml(t.tag)} | ${escapeHtml(t.desc)} |
ONLYOFFICE server is not running. Start it with: cd /root/workspace/cv-app && docker compose up -d onlyoffice-server
Cannot connect to ONLYOFFICE service. Make sure the Carbone+ONLYOFFICE backend is running.
'; } } // Load templates list for ONLYOFFICE editing async function loadOnlyofficeTemplates() { const list = document.getElementById('onlyoffice-templates-list'); if (!list) return; // Check if ONLYOFFICE is online first try { const healthResp = await fetch(ONLYOFFICE_API + '/health'); const health = await healthResp.json(); if (health.onlyoffice !== 'healthy') { list.innerHTML = 'ONLYOFFICE server is ' + health.onlyoffice + '. Start the container to enable inline editing.
'; return; } } catch (e) { list.innerHTML = 'Cannot reach ONLYOFFICE service.
'; return; } // Load templates from Carbone registry try { const resp = await fetch(CARBONE_API + '/templates'); const data = await resp.json(); if (!data.templates || !data.templates.length) { list.innerHTML = 'No templates uploaded yet. Upload a .docx template first to edit it inline.
'; return; } list.innerHTML = data.templates.map(t => `Error: ' + e.message + '
'; } } // Force save the document function forceSaveOnlyoffice() { if (onlyofficeEditorInstance) { toast('Saving... changes will be stored automatically'); } } // Close the editor — full teardown so the next open is completely fresh function closeOnlyofficeEditor() { if (onlyofficeEditorInstance) { try { onlyofficeEditorInstance.destroyEditor(); } catch (e) {} onlyofficeEditorInstance = null; } closeModal(); // Delete the DocsAPI global and remove the script tag so the next // openOnlyofficeEditor call loads a completely fresh ONLYOFFICE API. // This is the key: destroyEditor() cleans the editor instance, but // the DocsAPI object itself retains internal state (websocket pools, // document session references) that causes the next editor to load // a stale document. Removing the script + global forces a fresh init. delete window.DocsAPI; const oldScript = document.querySelector('script[src*="api/documents/api.js"]'); if (oldScript) oldScript.remove(); loadOnlyofficeTemplates(); } // Open ONLYOFFICE editor in a modal async function openOnlyofficeEditor(templateId, templateName) { if (onlyofficeEditorInstance) { try { onlyofficeEditorInstance.destroyEditor(); } catch (e) {} onlyofficeEditorInstance = null; } try { // Fetch JWT-signed config from backend (cache-busted) const resp = await fetch(ONLYOFFICE_API + '/' + templateId + '/config?cb=' + Date.now()); if (!resp.ok) throw new Error((await resp.json()).error || 'Failed to get config'); const { config, onlyofficeUrl } = await resp.json(); // Unique container ID per session const containerId = 'oo-editor-' + Date.now(); const body = `Changes are saved automatically. Click "Save" to force-save, or "Close Editor" to return to the template list.
`; showModal(body, '', 'xlarge'); // Load ONLYOFFICE API script fresh each time with cache-buster. // This ensures a completely new DocsAPI instance with no stale state. const scriptUrl = onlyofficeUrl + '/web-apps/apps/api/documents/api.js?cb=' + Date.now(); await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = scriptUrl; s.onload = resolve; s.onerror = () => reject(new Error('Failed to load ONLYOFFICE API')); document.head.appendChild(s); }); if (typeof DocsAPI === 'undefined' || !DocsAPI.DocEditor) { throw new Error('ONLYOFFICE API not loaded'); } onlyofficeEditorInstance = new DocsAPI.DocEditor(containerId, { ...config, width: '100%', height: '800px', events: { onSave: function() { toast('Document saved'); loadOnlyofficeTemplates(); }, onError: function(event) { toast('Editor error: ' + (event.data?.errorDescription || 'unknown'), 'error'); }, }, }); } catch (e) { toast('Failed to open editor: ' + e.message, 'error'); } } // Initialize when templates page is active document.addEventListener('DOMContentLoaded', () => { const templatesPage = document.getElementById('page-templates'); if (!templatesPage) return; if (templatesPage.classList.contains('active')) { checkOnlyofficeStatus(); loadOnlyofficeTemplates(); } const observer = new MutationObserver(() => { if (templatesPage.classList.contains('active')) { checkOnlyofficeStatus(); loadOnlyofficeTemplates(); } }); observer.observe(templatesPage, { attributes: true, attributeFilter: ['class'] }); });