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.
This commit is contained in:
parent
dcc2c07111
commit
f4d9c9f2ec
3 changed files with 403 additions and 399 deletions
154
app/static/js/settings-sync.js
Normal file
154
app/static/js/settings-sync.js
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
(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 =
|
||||
'<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 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 =
|
||||
'<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 — ' +
|
||||
'<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();
|
||||
});
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue