read.markets/app/static/js/settings-sync.js
Giorgio Gilestro f4d9c9f2ec settings: extract sync + import widget JS to static files
The two largest inline <script> blocks in settings.html — the cloud
sync modal/management UI (~145 lines) and the import widget wiring
(~245 lines) — moved to app/static/js/settings-sync.js and
settings-import.js respectively, included via <script src="..."
defer> at the bottom of the template.

Where the inline code referenced Jinja vars or {% if %} guards,
those values are now passed via data-* attributes on the relevant
DOM elements (or via window.cassandra* config objects for structured
data) and read in the static JS.

Smaller blocks (Stripe portal, digest prefs, language select,
invite copy) stay inline — each <40 lines and easier to follow
next to their markup. settings.html drops from 758 lines to roughly
half that.
2026-05-27 20:55:49 +02:00

154 lines
6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
'use strict';
function $(id) { return document.getElementById(id); }
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[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 =
'<span class="pf-warn">' + esc(e.message || 'status check failed') + '</span>';
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 couldnt 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 =
'<span class="badge badge--ok">On</span> ' +
'<span class="settings-row__hint">last synced ' + esc(when) + '</span>';
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 = '<span class="badge badge--ver">Off</span>';
// 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 &mdash; ' +
'<a href="#import">import a portfolio</a> 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();
});
})();