feat(dashboard): account and character pages update without reloading

Actions on an account (ban, mute, lock, GM level, email) and on a
character (edit, restoring a version, saving its XML) show the result
in place (reloadInPlace) instead of reloading the page. Deleting an
account goes back to Accounts without leaving the deleted account in
the history. After an in-place update (dash:refreshed) the pages show
the server's values again the way they do on load: last logins and the
mute end in the browser's time, and the GM level picker (unless a new
level is being picked).

Check: lock and unlock an account: the badge changes with no reload.
Change an account's GM level from another tab: the first tab's level
picker follows. Edit a character's items: the page updates when the
dialog closes. A character's last login updates when they log in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 02:00:59 -05:00
parent 172706ee51
commit c70b163701
3 changed files with 27 additions and 14 deletions

View File

@@ -132,7 +132,7 @@
var body = buildEdit();
if (!confirm('Save these changes? The player is disconnected if online.')) return;
api.action('/api/characters/' + config.id + '/edit', body).then(function (d) { return d.result; }).then(function (r) {
if (r && r.success) { bootstrap.Modal.getInstance(document.getElementById('editModal')).hide(); setTimeout(function () { location.reload(); }, 600); }
if (r && r.success) { bootstrap.Modal.getInstance(document.getElementById('editModal')).hide(); setTimeout(reloadInPlace, 600); }
}).catch(function () {});
});
}
@@ -205,7 +205,7 @@
var restore = e.target.closest('[data-restore]');
if (!restore || !confirm('Put the character back to this version? The player is disconnected if online, and the current version is kept in the history.')) return;
api.action('/api/snapshots/' + restore.dataset.restore + '/restore', {}).then(function (d) { return d.result; }).then(function (r) {
if (r && r.success) setTimeout(function () { location.reload(); }, 600);
if (r && r.success) setTimeout(reloadInPlace, 600);
}).catch(function () {});
});
}

View File

@@ -485,9 +485,19 @@ document.addEventListener('DOMContentLoaded', function () {
loadEmailStatus();
var ownEmail = document.getElementById('ownEmail');
if (ownEmail) ownEmail.value = accountEl.getAttribute('data-email') || '';
showServerValues();
// Update in place when someone else changes this account
Live.refreshPage('accounts', accountId);
});
// After an in-place update (nav.js) the server's values may have changed: show them the same way again
document.addEventListener('dash:refreshed', showServerValues);
// The GM level, mute and last logins as the server has them, shown in the browser's time
function showServerValues() {
var gmSelect = document.getElementById('gmLevelSelect');
if (gmSelect) {
gmSelect.value = accountEl.getAttribute('data-gm-level');
// Not while someone is picking a new level
if (gmSelect && (!gmSelect.dataset.shown || gmSelect.value === gmSelect.dataset.shown)) {
gmSelect.value = gmSelect.dataset.shown = accountEl.getAttribute('data-gm-level');
// Only operators may grant their own level or higher
Array.prototype.forEach.call(gmSelect.options, function (opt) {
if (DASH.gmLevel < 9 && parseInt(opt.value, 10) >= DASH.gmLevel) opt.disabled = true;
@@ -503,13 +513,11 @@ document.addEventListener('DOMContentLoaded', function () {
var ts = parseInt(el.getAttribute('data-ts'), 10);
el.textContent = ts > 0 ? fmt.unix(ts) : 'Never';
});
// Reload when someone else changes this account
Live.refreshPage('accounts', accountId);
});
}
// Reload once the change (and any live action it started, like a kick) has finished
// Show the page again once the change (and any live action it started, like a kick) has finished
function reloadOnSuccess(promise) {
promise.then(function (d) { return d.result || d; }).then(function () { location.reload(); }).catch(function () {});
promise.then(function (d) { return d.result || d; }).then(function () { reloadInPlace(); }).catch(function () {});
}
var reset2faBtn = document.getElementById('reset2faBtn');
@@ -610,7 +618,7 @@ function deleteAccount() {
if (!confirm('PERMANENTLY delete this account and all its characters? This cannot be undone.')) return;
if (!confirm('Are you absolutely sure?')) return;
}
api.action(base + '/delete', body).then(function (d) { return d.result; }).then(function () { window.location.href = accountEl.dataset.self ? '/login' : '/accounts'; }).catch(function () {});
api.action(base + '/delete', body).then(function (d) { return d.result; }).then(function () { if (accountEl.dataset.self) window.location.href = '/login'; else goTo('/accounts', { replace: true }); }).catch(function () {});
}
function resetPassword() {
var pwd = document.getElementById('newPwd').value;
@@ -643,7 +651,7 @@ function saveOwnEmail() {
pwd.value = '';
toast(d.message, 'success');
return d.result;
}).then(function () { location.reload(); }).catch(function () {});
}).then(function () { reloadInPlace(); }).catch(function () {});
}
function resendVerification() {
api.action('/api/account/email/resend', {}).catch(function () {});

View File

@@ -486,18 +486,23 @@
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// The last login in the browser's time, again after an in-place update (nav.js)
function showLastLogin() {
var el = document.getElementById('lastLogin');
if (el) {
var ts = parseInt(el.getAttribute('data-ts'));
el.textContent = ts > 0 ? new Date(ts * 1000).toLocaleString() : 'Never';
}
}
document.addEventListener('dash:refreshed', showLastLogin);
document.addEventListener('DOMContentLoaded', function() {
showLastLogin();
var inventory = document.getElementById('inventoryModal');
if (inventory) inventory.addEventListener('show.bs.modal', InventoryView.open, { once: true });
});
var characterId = document.getElementById('restrictions').getAttribute('data-character-id');
// Saves from the game (inventory, stats, level) and mail arrive live
// Saves from the game (inventory, stats, level) and mail arrive live, updating the page in place
if (window.Live) Live.refreshPage(['characters', 'mail'], characterId);
document.querySelectorAll('[data-restriction]').forEach(function (input) {
@@ -741,7 +746,7 @@ function saveXml(confirmed) {
if (!d.requestId) return;
Live.waitForAction(d.requestId).then(function (r) {
if (r.message) toast(r.message, r.success ? 'info' : 'danger');
if (r.success) location.reload();
if (r.success) reloadInPlace();
});
}).catch(function () {});
}