feat: web dashboard and playground work

The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 17:52:10 -05:00
parent 854d02f509
commit e213a7aeff
681 changed files with 88310 additions and 1321 deletions

View File

@@ -0,0 +1,107 @@
/**
* Your own account page: two-factor login setup and your characters' trade and mail history.
*/
(function () {
'use strict';
function $(id) { return document.getElementById(id); }
function show(id, visible) { $(id).classList.toggle('d-none', !visible); }
function showRecoveryCodes(codes) {
$('recoveryCodeList').textContent = codes.join('\n');
show('recoveryCodes', true);
$('recoveryCodes').scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function renderQr(uri) {
var box = $('twoFactorQr');
box.textContent = '';
if (!window.qrcode) return;
var qr = qrcode(0, 'M');
qr.addData(uri);
qr.make();
// SVG with a quiet zone, drawn dark-on-light so phone cameras read it on the dark theme
box.innerHTML = qr.createSvgTag({ cellSize: 4, margin: 4, scalable: true });
}
function load() {
return api.get('/api/account/2fa').then(function (s) {
show('twoFactorRequired', s.required && !s.enabled);
show('twoFactorOff', !s.enabled);
show('twoFactorOn', s.enabled);
show('twoFactorSetup', false);
$('twoFactorBadge').innerHTML = s.enabled ? fmt.badge('On', 'success') : fmt.badge('Off', s.required ? 'warning' : 'secondary');
if (s.enabled) {
$('twoFactorInfo').textContent = 'On since ' + fmt.unix(s.enabledAt) + '. ' + s.recoveryCodesLeft + ' recovery code' + (s.recoveryCodesLeft === 1 ? '' : 's') + ' left.';
show('twoFactorDisableGroup', !s.required);
}
});
}
$('twoFactorStart').addEventListener('click', function () {
api.action('/api/account/2fa/setup').then(function (r) {
$('twoFactorSecret').textContent = r.secret.replace(/(.{4})/g, '$1 ').trim();
renderQr(r.uri);
show('twoFactorOff', false);
show('twoFactorSetup', true);
$('twoFactorEnableCode').focus();
}).catch(function () {});
});
$('twoFactorEnable').addEventListener('click', function () {
api.action('/api/account/2fa/enable', { code: $('twoFactorEnableCode').value.trim() }).then(function (r) {
toast(r.message, 'success');
showRecoveryCodes(r.recoveryCodes);
load();
}).catch(function () {});
});
$('twoFactorNewCodes').addEventListener('click', function () {
api.action('/api/account/2fa/recovery_codes', { code: $('twoFactorManageCode').value.trim() }).then(function (r) {
showRecoveryCodes(r.recoveryCodes);
$('twoFactorManageCode').value = '';
load();
}).catch(function () {});
});
$('twoFactorDisable').addEventListener('click', function () {
if (!confirm('Turn off two-factor login? Your password alone will be enough to sign in.')) return;
api.action('/api/account/2fa/disable', { password: $('twoFactorDisablePwd').value, code: $('twoFactorManageCode').value.trim() }, 'Two-factor login is off')
.then(function () {
$('twoFactorDisablePwd').value = '';
$('twoFactorManageCode').value = '';
show('recoveryCodes', false);
load();
}).catch(function () {});
});
$('recoveryCopy').addEventListener('click', function () {
navigator.clipboard.writeText($('recoveryCodeList').textContent).then(function () { toast('Copied', 'success'); });
});
// ---- Trade and mail history ----
// Names come from the server's enum (Labels); only the colours are kept here
var METHOD_COLOURS = { 1: 'info', 2: 'secondary', 3: 'success', 4: 'light' };
function methodBadge(value) { return fmt.badge(Labels.name('transferMethods', value) || '?', METHOD_COLOURS[value] || 'secondary'); }
function characterName(id, name) { return esc(name || id); }
if (window.jQuery && document.getElementById('ownTransfers')) {
serverTable('#ownTransfers', '/api/account/transfers', [
{ data: 'time', orderable: false, render: function (d) { return esc(fmt.unix(d)); } },
{ data: 'method', orderable: false, render: function (d) { return methodBadge(d); } },
{ data: 'lot', orderable: false, render: function (d, t, row) {
if (!d) return esc(Number(row.coins).toLocaleString()) + ' coins';
var what = esc((row.count > 1 ? row.count + '× ' : '') + (row.name || 'LOT ' + d));
// Staff who can see reports get the item's full trace
return DASH.can('reports_view') && row.item_id && row.item_id !== '0' ? '<a href="/reports?object=' + esc(row.item_id) + '" title="Trace this item">' + what + '</a>' : what;
} },
{ data: 'from_character', orderable: false, render: function (d, t, row) { return characterName(d, row.from_name); } },
{ data: 'to_character', orderable: false, render: function (d, t, row) { return characterName(d, row.to_name); } }
], { dataTable: { searching: false, ordering: false, pageLength: 10, lengthChange: false }, liveTable: 'economy' });
}
if (location.hash === '#two-factor') $('two-factor').scrollIntoView();
load();
})();