(function () { 'use strict'; function $(id) { return document.getElementById(id); } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } document.addEventListener('DOMContentLoaded', function () { if (!window.CassandraSync) return; const statusEl = $('sync-status'); const actionsEl = $('sync-actions'); const feedbackEl = $('sync-feedback'); const modal = $('sync-modal'); const pin1 = $('sync-pin1'); const pin2 = $('sync-pin2'); const ack = $('sync-ack'); const errEl = $('sync-modal-err'); function setFeedback(msg, ok) { feedbackEl.style.color = ok ? 'var(--positive)' : ''; feedbackEl.textContent = msg || ''; } // External callers (the Import section above) can pass a callback // that fires after a successful enable-and-push. let pendingOnSuccess = null; function openModal(opts) { pendingOnSuccess = (opts && opts.onSuccess) || null; modal.style.display = 'flex'; // Focus PIN field after the layout flush so the caret lands. setTimeout(() => pin1.focus(), 0); } function closeModal() { modal.style.display = 'none'; pin1.value = ''; pin2.value = ''; ack.checked = false; errEl.hidden = true; pendingOnSuccess = null; } $('sync-modal-cancel').addEventListener('click', closeModal); // Backdrop click + Esc key dismiss the modal. modal.addEventListener('click', function (e) { if (e.target === modal) closeModal(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && modal.style.display !== 'none') closeModal(); }); $('sync-modal-form').addEventListener('submit', async function (e) { e.preventDefault(); errEl.hidden = true; if (pin1.value !== pin2.value) { errEl.textContent = 'PINs do not match.'; errEl.hidden = false; return; } if (pin1.value.length < 4) { errEl.textContent = 'PIN must be at least 4 characters.'; errEl.hidden = false; return; } const pie = JSON.parse(localStorage.getItem('cassandra.pie') || 'null'); if (!pie) { errEl.textContent = 'No portfolio in this browser yet. Import a CSV first, then enable sync.'; errEl.hidden = false; return; } try { await window.CassandraSync.pushSync(pie, pin1.value); const cb = pendingOnSuccess; closeModal(); // clears pendingOnSuccess await refresh(); setFeedback('Cloud sync enabled. Your encrypted portfolio is stored.', true); if (typeof cb === 'function') { try { cb(); } catch (cbErr) { console.warn('sync onSuccess threw', cbErr); } } } catch (e2) { errEl.textContent = e2.message || 'Failed to enable sync.'; errEl.hidden = false; } }); async function refresh() { let status; try { status = await window.CassandraSync.getStatus(); } catch (e) { statusEl.querySelector('.settings-row__value').innerHTML = '' + esc(e.message || 'status check failed') + ''; return; } const valueEl = statusEl.querySelector('.settings-row__value'); actionsEl.innerHTML = ''; if (status.exists && status.orphaned) { // The stored blob can no longer be decrypted (server key rotated // since it was written). The data is permanently unrecoverable, // so silently clean up the dead row and re-render in the // standard "off" state — leaving a soft one-liner so the user // knows why they need to re-import. try { await window.CassandraSync.disableSync(); } catch (e) { console.warn('auto-clear stale sync failed', e); } setFeedback('Your previous cloud backup couldn’t be restored. Re-import your portfolio to enable cloud sync again.', true); await refresh(); return; } else if (status.exists) { const when = status.updated_at ? new Date(status.updated_at).toISOString().replace('T', ' ').slice(0, 16) + ' UTC' : '—'; valueEl.innerHTML = 'On ' + 'last synced ' + esc(when) + ''; const disable = document.createElement('button'); disable.type = 'button'; disable.className = 'pf-secondary'; disable.textContent = 'Disable sync'; disable.addEventListener('click', async function () { if (!confirm('Remove your encrypted portfolio from the server? Your local copy is untouched.')) return; try { await window.CassandraSync.disableSync(); await refresh(); setFeedback('Cloud sync disabled. Server copy removed.', true); } catch (e) { setFeedback(e.message || 'Disable failed.', false); } }); actionsEl.appendChild(disable); } else { valueEl.innerHTML = 'Off'; // Only offer 'Enable' when there's actually a pie to encrypt; // otherwise the user would hit a dead-end at the modal. const hasPie = !!localStorage.getItem('cassandra.pie'); if (!hasPie) { const hint = document.createElement('span'); hint.className = 'settings-row__hint'; hint.innerHTML = 'Nothing to sync yet — ' + 'import a portfolio first, then come back to enable cloud sync.'; actionsEl.appendChild(hint); return; } const enable = document.createElement('button'); enable.type = 'button'; enable.textContent = 'Enable cloud sync'; enable.addEventListener('click', openModal); actionsEl.appendChild(enable); } } // Hooks for the Import section to drive this modal + status row. window.cassandraOpenSyncModal = openModal; window.cassandraRefreshSyncStatus = refresh; refresh(); }); })();