mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-10 08:44:20 +00:00
Add dashboard audit log and configuration management
- Implemented dashboard audit logging with InsertAuditLog, GetRecentAuditLogs, GetAuditLogsByIP, and CleanupOldAuditLogs methods. - Created dashboard configuration management with GetDashboardConfig and SetDashboardConfig methods. - Added new tables for dashboard_audit_log and dashboard_config in both MySQL and SQLite migrations. - Updated CMakeLists to include Crow and ASIO for dashboard server functionality. - Enhanced existing database classes to support new dashboard features, including character, play key, and property management. - Added new methods for retrieving and managing play keys, properties, and pet names. - Updated TestSQLDatabase to include stubs for new dashboard-related methods. - Modified shared and dashboard configuration files for new settings.
This commit is contained in:
102
dDashboardServer/templates/about.html
Normal file
102
dDashboardServer/templates/about.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
About DarkflameServer Dashboard
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Dashboard Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h4 class="mb-3">DarkflameServer Web Dashboard</h4>
|
||||
<p class="lead">
|
||||
A modern C++ web interface for managing your Darkflame Universe server.
|
||||
</p>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>Features</h5>
|
||||
<ul>
|
||||
<li><strong>Account Management:</strong> Create, modify, ban, lock, and mute player accounts</li>
|
||||
<li><strong>Character Management:</strong> View, rescue, and manage player characters</li>
|
||||
<li><strong>Moderation Tools:</strong> Approve pet names, manage properties, and review bug reports</li>
|
||||
<li><strong>Mail System:</strong> Send in-game mail to players with item attachments</li>
|
||||
<li><strong>Play Keys:</strong> Manage registration keys for new accounts</li>
|
||||
<li><strong>Activity Logs:</strong> Monitor player activity and track logins/logouts</li>
|
||||
<li><strong>Audit Trail:</strong> Track all administrative actions for accountability</li>
|
||||
</ul>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>Technology Stack</h5>
|
||||
<ul>
|
||||
<li><strong>Backend:</strong> C++ with Crow web framework</li>
|
||||
<li><strong>Frontend:</strong> Bootstrap 5, jQuery, DataTables</li>
|
||||
<li><strong>Templates:</strong> Mustache templating engine</li>
|
||||
<li><strong>Database:</strong> MySQL/MariaDB or SQLite</li>
|
||||
</ul>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>GM Levels</h5>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Level 0</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-secondary">Civilian</span> - Regular player</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 1</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-info">Forum Moderator</span> - Forum moderation only</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 2</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-primary">Junior Moderator</span> - Basic moderation tools</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 3</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-success">Moderator</span> - Full moderation access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 4</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-success">Senior Moderator</span> - Advanced moderation</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 5</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-warning">Lead Moderator</span> - Moderation leadership</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 6</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-warning">Junior Developer</span> - Development access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 7</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-warning">Inactive Developer</span> - Limited dev access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 8</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-danger">Developer</span> - Full development access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 9</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-danger">Operator</span> - Full system access</dd>
|
||||
</dl>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>About Darkflame Universe</h5>
|
||||
<p>
|
||||
DarkflameServer is an open-source server emulator for LEGO Universe,
|
||||
a massively multiplayer online game that was officially discontinued in 2012.
|
||||
The Darkflame Universe project aims to preserve and revive this beloved game
|
||||
for fans to continue enjoying.
|
||||
</p>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-start mt-4">
|
||||
<a href="https://github.com/DarkflameUniverse/DarkflameServer" target="_blank" class="btn btn-primary">
|
||||
<i class="bi bi-github"></i> GitHub Repository
|
||||
</a>
|
||||
<a href="https://github.com/DarkflameUniverse/DarkflameServer/tree/main/docs" target="_blank" class="btn btn-secondary">
|
||||
<i class="bi bi-book"></i> Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
162
dDashboardServer/templates/accounts/index.html
Normal file
162
dDashboardServer/templates/accounts/index.html
Normal file
@@ -0,0 +1,162 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-people"></i>
|
||||
Account Management
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">All Accounts</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="accounts-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>GM Level</th>
|
||||
<th>Banned</th>
|
||||
<th>Locked</th>
|
||||
<th>Muted Until</th>
|
||||
<th>Play Key ID</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Populated via DataTables Ajax -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Wait for jQuery + DataTables to be available without copying libraries locally.
|
||||
// Poll for a limited time and show a helpful error if they fail to load.
|
||||
function showLibraryError(message) {
|
||||
const el = document.getElementById('accounts-table');
|
||||
if (el) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'alert alert-danger';
|
||||
wrapper.textContent = message;
|
||||
el.replaceWith(wrapper);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the same pattern as Recent Activity: wait for DOMContentLoaded, check auth, then fetch data
|
||||
function loadAccounts() {
|
||||
API.get('/api/auth/status').then(status => {
|
||||
if (!status || !status.authenticated || status.gm_level < 3) {
|
||||
showLibraryError('You do not have permission to view accounts. Please log in with sufficient GM level.');
|
||||
return;
|
||||
}
|
||||
|
||||
API.get('/api/accounts').then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#accounts-table')) {
|
||||
const table = $('#accounts-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
const table = $('#accounts-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name', render: function(d, t, row) { return `<a href="/accounts/view/${row.id}">${d}</a>`; } },
|
||||
{ data: 'gm_level', render: function(d) { const badges={0:'secondary',1:'info',2:'primary',3:'success',4:'success',5:'warning',6:'warning',7:'warning',8:'danger',9:'danger'}; return `<span class="badge bg-${badges[d]||'secondary'}">${d}</span>`; } },
|
||||
{ data: 'banned', render: d => d ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>' },
|
||||
{ data: 'locked', render: d => d ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>' },
|
||||
{ data: 'mute_expire', render: function(d) { if (!d || d === 0) return '-'; return new Date(d * 1000).toLocaleString(); } },
|
||||
{ data: 'play_key_id' },
|
||||
{ data: null, orderable: false, render: function(data, type, row) {
|
||||
return `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="/accounts/view/${row.id}" class="btn btn-info" title="View"><i class="bi bi-eye"></i></a>
|
||||
<button data-account-id="${row.id}" class="btn btn-warning js-toggle-lock" title="Lock/Unlock"><i class="bi bi-lock"></i></button>
|
||||
<button data-account-id="${row.id}" class="btn btn-danger js-toggle-ban" title="Ban/Unban"><i class="bi bi-slash-circle"></i></button>
|
||||
<button data-account-id="${row.id}" class="btn btn-secondary js-mute-account" title="Mute"><i class="bi bi-mic-mute"></i></button>
|
||||
</div>`;
|
||||
} }
|
||||
],
|
||||
pageLength: 25,
|
||||
order: [[0, 'asc']],
|
||||
processing: true
|
||||
});
|
||||
|
||||
// Delegated event handlers
|
||||
$('#accounts-table').on('click', '.js-toggle-lock', function() { const id = $(this).data('account-id'); toggleLock(id, table); });
|
||||
$('#accounts-table').on('click', '.js-toggle-ban', function() { const id = $(this).data('account-id'); toggleBan(id, table); });
|
||||
$('#accounts-table').on('click', '.js-mute-account', function() { const id = $(this).data('account-id'); muteAccount(id, table); });
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
const msg = err && err.message ? err.message : 'Failed to load accounts';
|
||||
showLibraryError(`Error loading accounts: ${msg}`);
|
||||
});
|
||||
|
||||
}).catch(err => {
|
||||
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when jQuery/DataTables and API are ready
|
||||
safeInit(function($) {
|
||||
loadAccounts();
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
|
||||
async function toggleLock(accountId, table) {
|
||||
if (!confirm('Are you sure you want to toggle the lock status for this account?')) return;
|
||||
try {
|
||||
const result = await API.post(`/api/accounts/${accountId}/lock`);
|
||||
if (result.success) {
|
||||
if (table && table.ajax) table.ajax.reload();
|
||||
showAlert('success', 'Account lock status updated');
|
||||
} else {
|
||||
showAlert('danger', result.error || 'Failed to update account');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('danger', error.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBan(accountId, table) {
|
||||
if (!confirm('Are you sure you want to toggle the ban status for this account?')) return;
|
||||
try {
|
||||
const result = await API.post(`/api/accounts/${accountId}/ban`);
|
||||
if (result.success) {
|
||||
if (table && table.ajax) table.ajax.reload();
|
||||
showAlert('success', 'Account ban status updated');
|
||||
} else {
|
||||
showAlert('danger', result.error || 'Failed to update account');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('danger', error.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
async function muteAccount(accountId, table) {
|
||||
const days = prompt('Enter number of days to mute (0 to unmute):');
|
||||
if (days === null) return;
|
||||
try {
|
||||
const result = await API.post(`/api/accounts/${accountId}/mute`, { days: parseInt(days) });
|
||||
if (result.success) {
|
||||
if (table && table.ajax) table.ajax.reload();
|
||||
showAlert('success', 'Account mute status updated');
|
||||
} else {
|
||||
showAlert('danger', result.error || 'Failed to update account');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('danger', error.message || error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
214
dDashboardServer/templates/accounts/view.html
Normal file
214
dDashboardServer/templates/accounts/view.html
Normal file
@@ -0,0 +1,214 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4"><i class="bi bi-person-circle"></i> Account Details</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Account Info</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">ID</dt><dd class="col-sm-8" id="acct-id">-</dd>
|
||||
<dt class="col-sm-4">Username</dt><dd class="col-sm-8" id="acct-name">-</dd>
|
||||
<dt class="col-sm-4">Email</dt><dd class="col-sm-8" id="acct-email">-</dd>
|
||||
<dt class="col-sm-4">GM Level</dt><dd class="col-sm-8" id="acct-gm">-</dd>
|
||||
<dt class="col-sm-4">Banned</dt><dd class="col-sm-8" id="acct-banned">-</dd>
|
||||
<dt class="col-sm-4">Locked</dt><dd class="col-sm-8" id="acct-locked">-</dd>
|
||||
<dt class="col-sm-4">Mute Expire</dt><dd class="col-sm-8" id="acct-mute">-</dd>
|
||||
<dt class="col-sm-4">Play Key ID</dt><dd class="col-sm-8" id="acct-playkey">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Administrative Actions</div>
|
||||
<div class="card-body">
|
||||
<button class="btn btn-danger mb-2" id="delete-account">Delete Account</button>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Set GM Level</label>
|
||||
<input type="number" id="gm-level-input" class="form-control" min="0" max="9">
|
||||
<button class="btn btn-primary mt-2" id="set-gm">Update GM Level</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Update Email</label>
|
||||
<input type="email" id="email-input" class="form-control">
|
||||
<button class="btn btn-primary mt-2" id="set-email">Update Email</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reset Password</label>
|
||||
<input type="password" id="password-input" class="form-control">
|
||||
<button class="btn btn-primary mt-2" id="reset-password">Reset Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Characters</div>
|
||||
<div class="card-body">
|
||||
<table id="characters-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Level</th>
|
||||
<th>Map</th>
|
||||
<th>Last Login</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Sessions</div>
|
||||
<div class="card-body">
|
||||
<table id="sessions-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>IP Address</th>
|
||||
<th>Login Time</th>
|
||||
<th>Logout Time</th>
|
||||
<th>Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const accountId = (window.location.pathname.split('/').pop() || '').trim();
|
||||
|
||||
async function loadAccount() {
|
||||
try {
|
||||
const res = await API.get(`/api/accounts/${accountId}`);
|
||||
if (res && res.success) {
|
||||
$('#acct-id').text(res.id);
|
||||
$('#acct-name').text(res.name);
|
||||
$('#acct-email').text(res.email || '-');
|
||||
$('#acct-gm').text(res.gm_level);
|
||||
$('#acct-banned').html(res.banned ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
|
||||
$('#acct-locked').html(res.locked ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>');
|
||||
$('#acct-mute').text(res.mute_expire && res.mute_expire>0 ? new Date(res.mute_expire*1000).toLocaleString() : '-');
|
||||
$('#acct-playkey').text(res.play_key_id || '-');
|
||||
$('#gm-level-input').val(res.gm_level);
|
||||
$('#email-input').val(res.email || '');
|
||||
// Load related data
|
||||
loadCharacters();
|
||||
loadSessions();
|
||||
} else {
|
||||
alert(res.error || 'Failed to load account');
|
||||
}
|
||||
} catch (err) { alert(err.message); }
|
||||
}
|
||||
|
||||
async function loadCharacters() {
|
||||
try {
|
||||
const res = await API.get(`/api/accounts/${accountId}/characters`);
|
||||
const data = (res && Array.isArray(res.data)) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#characters-table')) {
|
||||
const table = $('#characters-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
$('#characters-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name', render: function(d, t, row) { return `<a href="/characters/view/${row.id}">${d}</a>`; } },
|
||||
{ data: 'level' },
|
||||
{ data: 'map_id' },
|
||||
{ data: 'last_login', render: d => d ? new Date(d * 1000).toLocaleString() : '-' }
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
pageLength: 10
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load characters', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSessions() {
|
||||
try {
|
||||
const res = await API.get(`/api/accounts/${accountId}/sessions`);
|
||||
const data = (res && Array.isArray(res.data)) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#sessions-table')) {
|
||||
const table = $('#sessions-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
$('#sessions-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'session_id' },
|
||||
{ data: 'ip_address' },
|
||||
{ data: 'login_time', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
|
||||
{ data: 'logout_time', render: d => d && d>0 ? new Date(d * 1000).toLocaleString() : '-' },
|
||||
{ data: 'active', render: d => d ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>' }
|
||||
],
|
||||
order: [[2, 'desc']],
|
||||
pageLength: 10
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when libraries are ready (API used, jQuery optional)
|
||||
safeInit(function($) {
|
||||
loadAccount();
|
||||
|
||||
document.getElementById('delete-account').addEventListener('click', async function() {
|
||||
if (!confirm('Delete this account? This action is irreversible.')) return;
|
||||
try {
|
||||
const res = await API.post(`/api/accounts/${accountId}/delete`, {});
|
||||
if (res && res.success) {
|
||||
alert('Account deleted');
|
||||
window.location.href = '/accounts';
|
||||
} else {
|
||||
alert(res.error || 'Failed to delete');
|
||||
}
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('set-gm').addEventListener('click', async function() {
|
||||
const lvl = parseInt(document.getElementById('gm-level-input').value);
|
||||
try {
|
||||
const res = await API.post(`/api/accounts/${accountId}/gm-level`, { gm_level: lvl });
|
||||
if (res && res.success) { alert('GM level updated'); loadAccount(); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('set-email').addEventListener('click', async function() {
|
||||
const email = document.getElementById('email-input').value.trim();
|
||||
try {
|
||||
const res = await API.post(`/api/accounts/${accountId}/email`, { email: email });
|
||||
if (res && res.success) { alert('Email updated'); loadAccount(); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('reset-password').addEventListener('click', async function() {
|
||||
const pw = document.getElementById('password-input').value;
|
||||
if (!pw || pw.length < 8) { alert('Password must be at least 8 characters'); return; }
|
||||
try {
|
||||
const res = await API.post(`/api/accounts/${accountId}/password-reset`, { password: pw });
|
||||
if (res && res.success) { alert('Password reset'); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
</script>
|
||||
151
dDashboardServer/templates/bugreports/index.html
Normal file
151
dDashboardServer/templates/bugreports/index.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4"><i class="bi bi-bug"></i> Bug Reports</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="list-group" id="report-filter">
|
||||
<button class="list-group-item list-group-item-action active" data-status="all">All</button>
|
||||
<button class="list-group-item list-group-item-action" data-status="unresolved">Unresolved</button>
|
||||
<button class="list-group-item list-group-item-action" data-status="resolved">Resolved</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Reports</div>
|
||||
<div class="card-body">
|
||||
<table id="bugreports-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Character</th>
|
||||
<th>Submitted</th>
|
||||
<th>Resolved</th>
|
||||
<th>Summary</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for resolving -->
|
||||
<div class="modal" tabindex="-1" id="resolveModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Resolve Bug Report</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Resolution Message</label>
|
||||
<textarea id="resolution-text" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="resolve-confirm">Resolve</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentStatus = 'all';
|
||||
let currentResolveId = 0;
|
||||
|
||||
function loadTable() {
|
||||
API.get('/api/bugreports', { status: currentStatus }).then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#bugreports-table')) {
|
||||
const table = $('#bugreports-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
$('#bugreports-table').DataTable({
|
||||
data: data,
|
||||
destroy: true,
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'character_name' },
|
||||
{ data: 'submitted', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
|
||||
{ data: 'resolved_time', render: d => d && d>0 ? new Date(d * 1000).toLocaleString() : '-' },
|
||||
{ data: 'body', render: d => d ? d.substring(0,120) : '-' },
|
||||
{ data: null, orderable: false, render: function(data, type, row) {
|
||||
let actions = '';
|
||||
if (!row.resolved_time || row.resolved_time == 0) {
|
||||
actions += `<button class="btn btn-sm btn-success" onclick="openResolve(${row.id})">Resolve</button>`;
|
||||
}
|
||||
actions += ` <button class="btn btn-sm btn-info" onclick="viewReport(${row.id})">View</button>`;
|
||||
return actions;
|
||||
} }
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
pageLength: 25
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
alert(err && err.message ? err.message : 'Failed to load bug reports');
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when libraries are ready
|
||||
safeInit(function($) {
|
||||
loadTable();
|
||||
|
||||
// Filter clicks
|
||||
$('#report-filter button').on('click', function() {
|
||||
$('#report-filter button').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
currentStatus = $(this).data('status');
|
||||
loadTable();
|
||||
});
|
||||
|
||||
// Resolve confirm
|
||||
$('#resolve-confirm').on('click', async function() {
|
||||
const resolution = $('#resolution-text').val().trim();
|
||||
if (!resolution) { alert('Resolution message required'); return; }
|
||||
try {
|
||||
const res = await API.post(`/api/bugreports/${currentResolveId}/resolve`, { resolution: resolution });
|
||||
if (res && res.success) {
|
||||
$('#resolveModal').modal('hide');
|
||||
loadTable();
|
||||
alert('Bug report resolved');
|
||||
} else {
|
||||
alert(res.error || 'Failed to resolve');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
});
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
|
||||
function openResolve(id) {
|
||||
currentResolveId = id;
|
||||
$('#resolution-text').val('');
|
||||
var modal = new bootstrap.Modal(document.getElementById('resolveModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function viewReport(id) {
|
||||
try {
|
||||
const res = await API.get(`/api/bugreports/${id}`);
|
||||
if (res && res.success) {
|
||||
const text = `ID: ${res.id}\nCharacter: ${res.character_name}\nSubmitted: ${res.submitted?new Date(res.submitted*1000).toLocaleString():''}\n\n${res.body}`;
|
||||
alert(text);
|
||||
} else {
|
||||
alert(res.error || 'Failed to get report');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
163
dDashboardServer/templates/characters/index.html
Normal file
163
dDashboardServer/templates/characters/index.html
Normal file
@@ -0,0 +1,163 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-person-badge"></i>
|
||||
Character Management
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">All Characters</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="characters-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Account</th>
|
||||
<th>Name</th>
|
||||
<th>Pending Name</th>
|
||||
<th>Needs Rename</th>
|
||||
<th>Last Login</th>
|
||||
<th>Permission Map</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Populated via DataTables Ajax -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Wait for jQuery + DataTables to be available
|
||||
function showLibraryError(message) {
|
||||
const el = document.getElementById('characters-table');
|
||||
if (el) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'alert alert-danger';
|
||||
wrapper.textContent = message;
|
||||
el.replaceWith(wrapper);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
function loadCharacters() {
|
||||
API.get('/api/auth/status').then(status => {
|
||||
if (!status || !status.authenticated || status.gm_level < 3) {
|
||||
showLibraryError('You do not have permission to view characters. Please log in with sufficient GM level.');
|
||||
return;
|
||||
}
|
||||
|
||||
API.get('/api/characters').then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#characters-table')) {
|
||||
const table = $('#characters-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
const table = $('#characters-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'account_name', render: function(d, t, row) {
|
||||
return row.account_id ? `<a href="/accounts/view/${row.account_id}">${d || row.account_id}</a>` : (d || '-');
|
||||
}},
|
||||
{ data: 'name', render: function(d, t, row) {
|
||||
return `<a href="/characters/view/${row.id}">${d}</a>`;
|
||||
}},
|
||||
{ data: 'pending_name', render: d => d || '-' },
|
||||
{ data: 'needs_rename', render: d => d ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>' },
|
||||
{ data: 'last_login', render: function(d) {
|
||||
if (!d || d === 0) return 'Never';
|
||||
return new Date(d * 1000).toLocaleString();
|
||||
}},
|
||||
{ data: 'permission_map', render: d => d || '-' },
|
||||
{ data: null, orderable: false, render: function(data, type, row) {
|
||||
return `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="/characters/view/${row.id}" class="btn btn-info" title="View"><i class="bi bi-eye"></i></a>
|
||||
<button data-char-id="${row.id}" class="btn btn-warning js-rescue-char" title="Rescue Character"><i class="bi bi-life-preserver"></i></button>
|
||||
<button data-char-id="${row.id}" class="btn btn-danger js-delete-char" title="Delete Character"><i class="bi bi-trash"></i></button>
|
||||
</div>`;
|
||||
}}
|
||||
],
|
||||
pageLength: 25,
|
||||
order: [[0, 'desc']],
|
||||
processing: true
|
||||
});
|
||||
|
||||
// Delegated event handlers
|
||||
$('#characters-table').on('click', '.js-rescue-char', function() {
|
||||
const id = $(this).data('char-id');
|
||||
rescueCharacter(id, table);
|
||||
});
|
||||
$('#characters-table').on('click', '.js-delete-char', function() {
|
||||
const id = $(this).data('char-id');
|
||||
deleteCharacter(id, table);
|
||||
});
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
const msg = err && err.message ? err.message : 'Failed to load characters';
|
||||
showLibraryError(`Error loading characters: ${msg}`);
|
||||
});
|
||||
|
||||
}).catch(err => {
|
||||
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when jQuery/DataTables and API are ready
|
||||
safeInit(function($) {
|
||||
loadCharacters();
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
|
||||
async function rescueCharacter(charId, table) {
|
||||
if (!confirm('Are you sure you want to rescue this character? This will move them to a safe location.')) return;
|
||||
try {
|
||||
const result = await API.post(`/api/characters/${charId}/rescue`);
|
||||
if (result.success) {
|
||||
showAlert('success', 'Character rescued successfully');
|
||||
if (table && table.ajax) table.ajax.reload();
|
||||
} else {
|
||||
showAlert('danger', result.error || 'Failed to rescue character');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('danger', error.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCharacter(charId, table) {
|
||||
const confirmMsg = 'Are you sure you want to DELETE this character? This action is irreversible!';
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
const doubleConfirm = prompt('Type "DELETE" to confirm:');
|
||||
if (doubleConfirm !== 'DELETE') {
|
||||
showAlert('info', 'Deletion cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await API.post(`/api/characters/${charId}/delete`);
|
||||
if (result.success) {
|
||||
showAlert('success', 'Character deleted');
|
||||
if (table && table.ajax) table.ajax.reload();
|
||||
} else {
|
||||
showAlert('danger', result.error || 'Failed to delete character');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('danger', error.message || error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
314
dDashboardServer/templates/characters/view.html
Normal file
314
dDashboardServer/templates/characters/view.html
Normal file
@@ -0,0 +1,314 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4"><i class="bi bi-person-badge"></i> Character Details</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Character Info</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">ID</dt><dd class="col-sm-7" id="char-id">-</dd>
|
||||
<dt class="col-sm-5">Name</dt><dd class="col-sm-7" id="char-name">-</dd>
|
||||
<dt class="col-sm-5">Pending Name</dt><dd class="col-sm-7" id="char-pending-name">-</dd>
|
||||
<dt class="col-sm-5">Account</dt><dd class="col-sm-7" id="char-account">-</dd>
|
||||
<dt class="col-sm-5">Level</dt><dd class="col-sm-7" id="char-level">-</dd>
|
||||
<dt class="col-sm-5">Universe Score</dt><dd class="col-sm-7" id="char-uscore">-</dd>
|
||||
<dt class="col-sm-5">Current Zone</dt><dd class="col-sm-7" id="char-zone">-</dd>
|
||||
<dt class="col-sm-5">Last Login</dt><dd class="col-sm-7" id="char-last-login">-</dd>
|
||||
<dt class="col-sm-5">Created</dt><dd class="col-sm-7" id="char-created">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Restrictions</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-5">Mail Restricted</dt><dd class="col-sm-7" id="char-mail-restricted">-</dd>
|
||||
<dt class="col-sm-5">Trade Restricted</dt><dd class="col-sm-7" id="char-trade-restricted">-</dd>
|
||||
<dt class="col-sm-5">Chat Restricted</dt><dd class="col-sm-7" id="char-chat-restricted">-</dd>
|
||||
<dt class="col-sm-5">Needs Rename</dt><dd class="col-sm-7" id="char-needs-rename">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Administrative Actions</div>
|
||||
<div class="card-body">
|
||||
<button class="btn btn-warning mb-2 w-100" id="rescue-char">
|
||||
<i class="bi bi-life-preserver"></i> Rescue Character
|
||||
</button>
|
||||
<button class="btn btn-primary mb-2 w-100" id="approve-name">
|
||||
<i class="bi bi-check-circle"></i> Approve Pending Name
|
||||
</button>
|
||||
<button class="btn btn-danger mb-2 w-100" id="delete-char">
|
||||
<i class="bi bi-trash"></i> Delete Character
|
||||
</button>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Toggle Mail Restriction</label>
|
||||
<button class="btn btn-secondary w-100" id="toggle-mail">Toggle Mail</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Toggle Trade Restriction</label>
|
||||
<button class="btn btn-secondary w-100" id="toggle-trade">Toggle Trade</button>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Toggle Chat Restriction</label>
|
||||
<button class="btn btn-secondary w-100" id="toggle-chat">Toggle Chat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="stats-tab" data-bs-toggle="tab" href="#stats" role="tab">Stats</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="inventory-tab" data-bs-toggle="tab" href="#inventory" role="tab">Inventory</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#activity" role="tab">Activity</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="stats" role="tabpanel">
|
||||
<h6>Character Statistics</h6>
|
||||
<div id="char-stats-content">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Total Currency Collected</dt><dd class="col-sm-6" id="stat-currency">-</dd>
|
||||
<dt class="col-sm-6">Total Bricks Collected</dt><dd class="col-sm-6" id="stat-bricks">-</dd>
|
||||
<dt class="col-sm-6">Total Smashables</dt><dd class="col-sm-6" id="stat-smashables">-</dd>
|
||||
<dt class="col-sm-6">Total Quick Builds</dt><dd class="col-sm-6" id="stat-quickbuilds">-</dd>
|
||||
<dt class="col-sm-6">Total Enemies Smashed</dt><dd class="col-sm-6" id="stat-enemies">-</dd>
|
||||
<dt class="col-sm-6">Total Rockets Used</dt><dd class="col-sm-6" id="stat-rockets">-</dd>
|
||||
<dt class="col-sm-6">Total Missions Completed</dt><dd class="col-sm-6" id="stat-missions">-</dd>
|
||||
<dt class="col-sm-6">Total Pets Tamed</dt><dd class="col-sm-6" id="stat-pets">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="inventory" role="tabpanel">
|
||||
<h6>Inventory Items</h6>
|
||||
<div id="char-inventory-content">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item ID</th>
|
||||
<th>Count</th>
|
||||
<th>Slot</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="inventory-tbody">
|
||||
<tr><td colspan="3" class="text-center">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="activity" role="tabpanel">
|
||||
<h6>Recent Activity</h6>
|
||||
<div id="char-activity-content">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Activity</th>
|
||||
<th>Map</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="activity-tbody">
|
||||
<tr><td colspan="3" class="text-center">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const characterId = (window.location.pathname.split('/').pop() || '').trim();
|
||||
|
||||
async function loadCharacter() {
|
||||
try {
|
||||
const res = await API.get(`/api/characters/${characterId}`);
|
||||
if (res && res.success) {
|
||||
$('#char-id').text(res.id);
|
||||
$('#char-name').text(res.name);
|
||||
$('#char-pending-name').text(res.pending_name || '-');
|
||||
|
||||
if (res.account_id) {
|
||||
$('#char-account').html(`<a href="/accounts/view/${res.account_id}">${res.account_name || res.account_id}</a>`);
|
||||
} else {
|
||||
$('#char-account').text('-');
|
||||
}
|
||||
|
||||
$('#char-level').text(res.level || 0);
|
||||
$('#char-uscore').text(res.uscore || 0);
|
||||
$('#char-zone').text(res.zone_id || '-');
|
||||
$('#char-last-login').text(res.last_login && res.last_login > 0 ? new Date(res.last_login * 1000).toLocaleString() : 'Never');
|
||||
$('#char-created').text(res.created_on ? new Date(res.created_on * 1000).toLocaleString() : '-');
|
||||
|
||||
// Restrictions
|
||||
$('#char-mail-restricted').html(res.mail_restricted ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
|
||||
$('#char-trade-restricted').html(res.trade_restricted ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
|
||||
$('#char-chat-restricted').html(res.chat_restricted ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
|
||||
$('#char-needs-rename').html(res.needs_rename ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>');
|
||||
|
||||
// Load related data
|
||||
loadCharacterStats();
|
||||
loadCharacterActivity();
|
||||
} else {
|
||||
alert(res.error || 'Failed to load character');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message || 'Error loading character');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCharacterStats() {
|
||||
try {
|
||||
const res = await API.get(`/api/characters/${characterId}/stats`);
|
||||
if (res && res.success) {
|
||||
$('#stat-currency').text(res.total_currency_collected || 0);
|
||||
$('#stat-bricks').text(res.total_bricks_collected || 0);
|
||||
$('#stat-smashables').text(res.total_smashables || 0);
|
||||
$('#stat-quickbuilds').text(res.total_quickbuilds_completed || 0);
|
||||
$('#stat-enemies').text(res.total_enemies_smashed || 0);
|
||||
$('#stat-rockets').text(res.total_rockets_used || 0);
|
||||
$('#stat-missions').text(res.total_missions_completed || 0);
|
||||
$('#stat-pets').text(res.total_pets_tamed || 0);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load character stats', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCharacterActivity() {
|
||||
try {
|
||||
const res = await API.get(`/api/characters/${characterId}/activity`);
|
||||
const data = (res && Array.isArray(res.data)) ? res.data : [];
|
||||
|
||||
const tbody = $('#activity-tbody');
|
||||
tbody.empty();
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.append('<tr><td colspan="3" class="text-center">No activity found</td></tr>');
|
||||
} else {
|
||||
data.forEach(activity => {
|
||||
const row = $('<tr>');
|
||||
row.append($('<td>').text(new Date(activity.timestamp * 1000).toLocaleString()));
|
||||
row.append($('<td>').text(activity.activity));
|
||||
row.append($('<td>').text(activity.map_id));
|
||||
tbody.append(row);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load character activity', err);
|
||||
$('#activity-tbody').html('<tr><td colspan="3" class="text-center text-danger">Failed to load activity</td></tr>');
|
||||
}
|
||||
}
|
||||
|
||||
// Load inventory when the tab is clicked
|
||||
$('#inventory-tab').on('shown.bs.tab', async function() {
|
||||
try {
|
||||
const res = await API.get(`/api/characters/${characterId}/inventory`);
|
||||
const data = (res && Array.isArray(res.data)) ? res.data : [];
|
||||
|
||||
const tbody = $('#inventory-tbody');
|
||||
tbody.empty();
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.append('<tr><td colspan="3" class="text-center">No items found</td></tr>');
|
||||
} else {
|
||||
data.forEach(item => {
|
||||
const row = $('<tr>');
|
||||
row.append($('<td>').text(item.item_id));
|
||||
row.append($('<td>').text(item.count || 1));
|
||||
row.append($('<td>').text(item.slot || '-'));
|
||||
tbody.append(row);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load inventory', err);
|
||||
$('#inventory-tbody').html('<tr><td colspan="3" class="text-center text-danger">Failed to load inventory</td></tr>');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize when libraries are ready
|
||||
safeInit(function($) {
|
||||
loadCharacter();
|
||||
|
||||
document.getElementById('rescue-char').addEventListener('click', async function() {
|
||||
if (!confirm('Rescue this character to a safe location?')) return;
|
||||
try {
|
||||
const res = await API.post(`/api/characters/${characterId}/rescue`, {});
|
||||
if (res && res.success) {
|
||||
alert('Character rescued');
|
||||
loadCharacter();
|
||||
} else {
|
||||
alert(res.error || 'Failed to rescue character');
|
||||
}
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('approve-name').addEventListener('click', async function() {
|
||||
try {
|
||||
const res = await API.post(`/api/characters/${characterId}/approve-name`, {});
|
||||
if (res && res.success) {
|
||||
alert('Name approved');
|
||||
loadCharacter();
|
||||
} else {
|
||||
alert(res.error || 'Failed to approve name');
|
||||
}
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('delete-char').addEventListener('click', async function() {
|
||||
const confirmMsg = 'DELETE this character? This action is irreversible!';
|
||||
if (!confirm(confirmMsg)) return;
|
||||
const doubleConfirm = prompt('Type "DELETE" to confirm:');
|
||||
if (doubleConfirm !== 'DELETE') return;
|
||||
try {
|
||||
const res = await API.post(`/api/characters/${characterId}/delete`, {});
|
||||
if (res && res.success) {
|
||||
alert('Character deleted');
|
||||
window.location.href = '/characters';
|
||||
} else {
|
||||
alert(res.error || 'Failed to delete');
|
||||
}
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('toggle-mail').addEventListener('click', async function() {
|
||||
try {
|
||||
const res = await API.post(`/api/characters/${characterId}/toggle-mail`, {});
|
||||
if (res && res.success) { alert('Mail restriction toggled'); loadCharacter(); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('toggle-trade').addEventListener('click', async function() {
|
||||
try {
|
||||
const res = await API.post(`/api/characters/${characterId}/toggle-trade`, {});
|
||||
if (res && res.success) { alert('Trade restriction toggled'); loadCharacter(); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
|
||||
document.getElementById('toggle-chat').addEventListener('click', async function() {
|
||||
try {
|
||||
const res = await API.post(`/api/characters/${characterId}/toggle-chat`, {});
|
||||
if (res && res.success) { alert('Chat restriction toggled'); loadCharacter(); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
});
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
</script>
|
||||
293
dDashboardServer/templates/index.html
Normal file
293
dDashboardServer/templates/index.html
Normal file
@@ -0,0 +1,293 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">Dashboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#is_authenticated}}
|
||||
<div class="row">
|
||||
<!-- Account Info Card -->
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-person-circle text-primary"></i>
|
||||
Your Account
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<strong>Username:</strong> {{username}}<br>
|
||||
<strong>Account ID:</strong> {{account_id}}<br>
|
||||
<strong>GM Level:</strong> {{gm_level}} ({{gm_level_name}})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#is_gm_3_plus}}
|
||||
<!-- Server Stats Card -->
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-server text-success"></i>
|
||||
Server Status
|
||||
</h5>
|
||||
<div id="server-stats">
|
||||
<p class="card-text">
|
||||
<strong>Master:</strong> <span id="master-status" class="badge bg-secondary">Loading...</span><br>
|
||||
<strong>Connected Clients:</strong> <span id="client-count">-</span><br>
|
||||
<strong>Packets Sent:</strong> <span id="packets-sent">-</span><br>
|
||||
<strong>Packets Received:</strong> <span id="packets-received">-</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accounts Card -->
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-people text-info"></i>
|
||||
Accounts
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<strong>Total Accounts:</strong> <span id="total-accounts">-</span><br>
|
||||
<strong>Banned:</strong> <span id="banned-accounts">-</span><br>
|
||||
<strong>Locked:</strong> <span id="locked-accounts">-</span>
|
||||
</p>
|
||||
<a href="/accounts" class="btn btn-sm btn-primary">Manage Accounts</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Characters Card -->
|
||||
<div class="col-md-6 col-lg-3 mb-4">
|
||||
<div class="card stats-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-person-badge text-warning"></i>
|
||||
Characters
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<strong>Total Characters:</strong> <span id="total-characters">-</span><br>
|
||||
<strong>Pending Names:</strong> <span id="pending-names">-</span>
|
||||
</p>
|
||||
<a href="/characters" class="btn btn-sm btn-primary">Manage Characters</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/is_gm_3_plus}}
|
||||
</div>
|
||||
|
||||
{{#is_gm_3_plus}}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-activity"></i>
|
||||
Recent Activity
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="recent-activity-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Character</th>
|
||||
<th>Activity</th>
|
||||
<th>Map</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Populated via API -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/is_gm_3_plus}}
|
||||
|
||||
<!-- Character Cards for All Authenticated Users -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-3">
|
||||
<i class="bi bi-person-badge"></i>
|
||||
Your Characters
|
||||
</h3>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="character-cards-container">
|
||||
<!-- Character cards will be populated via JavaScript -->
|
||||
<div class="col-12 text-center">
|
||||
<p class="text-muted">Loading characters...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/is_authenticated}}
|
||||
|
||||
{{^is_authenticated}}
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h3>Welcome to DarkflameServer Dashboard</h3>
|
||||
<p class="lead">Please log in to access the dashboard.</p>
|
||||
<a href="/login" class="btn btn-primary btn-lg">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/is_authenticated}}
|
||||
|
||||
<script>
|
||||
{{#is_gm_3_plus}}
|
||||
// Load dashboard stats
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
// Server stats
|
||||
const serverStats = await API.get('/api/stats/server');
|
||||
if (serverStats) {
|
||||
updateServerStats(serverStats);
|
||||
}
|
||||
|
||||
// Account stats
|
||||
const accountStats = await API.get('/api/stats/accounts');
|
||||
if (accountStats) {
|
||||
updateAccountStats(accountStats);
|
||||
}
|
||||
|
||||
// Character stats
|
||||
const characterStats = await API.get('/api/stats/characters');
|
||||
if (characterStats) {
|
||||
updateCharacterStats(characterStats);
|
||||
}
|
||||
|
||||
// Recent activity
|
||||
const activities = await API.get('/api/stats/recent-activity');
|
||||
if (activities && activities.data) {
|
||||
updateRecentActivity(activities.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update server stats on UI
|
||||
function updateServerStats(data) {
|
||||
document.getElementById('master-status').textContent = data.master_connected ? 'Connected' : 'Disconnected';
|
||||
document.getElementById('master-status').className = data.master_connected ? 'badge bg-success' : 'badge bg-danger';
|
||||
document.getElementById('client-count').textContent = data.connected_clients || 0;
|
||||
document.getElementById('packets-sent').textContent = data.packets_sent || 0;
|
||||
document.getElementById('packets-received').textContent = data.packets_received || 0;
|
||||
}
|
||||
|
||||
// Update account stats on UI
|
||||
function updateAccountStats(data) {
|
||||
document.getElementById('total-accounts').textContent = data.total || 0;
|
||||
document.getElementById('banned-accounts').textContent = data.banned || 0;
|
||||
document.getElementById('locked-accounts').textContent = data.locked || 0;
|
||||
}
|
||||
|
||||
// Update character stats on UI
|
||||
function updateCharacterStats(data) {
|
||||
document.getElementById('total-characters').textContent = data.total || 0;
|
||||
document.getElementById('pending-names').textContent = data.pending_names || 0;
|
||||
}
|
||||
|
||||
// Update recent activity table
|
||||
function updateRecentActivity(data) {
|
||||
// Avoid reinitialising the DataTable on repeated calls (e.g. interval refreshs).
|
||||
// If the table already exists, update its data and redraw. Otherwise initialize it.
|
||||
if ($.fn.DataTable.isDataTable('#recent-activity-table')) {
|
||||
const table = $('#recent-activity-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
const table = $('#recent-activity-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'timestamp' },
|
||||
{ data: 'character_name' },
|
||||
{ data: 'activity' },
|
||||
{ data: 'map_id' }
|
||||
],
|
||||
pageLength: 10,
|
||||
order: [[0, 'desc']]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initial load
|
||||
document.addEventListener('DOMContentLoaded', loadDashboardStats);
|
||||
|
||||
// Auto-refresh stats every 30 seconds
|
||||
setInterval(loadDashboardStats, 30000);
|
||||
{{/is_gm_3_plus}}
|
||||
|
||||
{{#is_authenticated}}
|
||||
// Load user's characters for character cards
|
||||
async function loadUserCharacters() {
|
||||
try {
|
||||
const res = await API.get('/api/user/characters');
|
||||
const characters = (res && Array.isArray(res.data)) ? res.data : (res || []);
|
||||
|
||||
const container = document.getElementById('character-cards-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (characters.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center">
|
||||
<p class="text-muted">You don't have any characters yet. Log in to the game to create one!</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
characters.forEach(char => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'col-md-6 col-lg-4 mb-4';
|
||||
card.innerHTML = `
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
${char.name}
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<strong>Level:</strong> ${char.level || 0}<br>
|
||||
<strong>Universe Score:</strong> ${char.uscore || 0}<br>
|
||||
<strong>Current Zone:</strong> ${char.zone_id || 'Unknown'}<br>
|
||||
<strong>Last Login:</strong> ${char.last_login && char.last_login > 0 ? new Date(char.last_login * 1000).toLocaleString() : 'Never'}
|
||||
</p>
|
||||
${char.pending_name ? `<span class="badge bg-warning mb-2">Pending Name: ${char.pending_name}</span><br>` : ''}
|
||||
${char.needs_rename ? '<span class="badge bg-danger mb-2">Needs Rename</span><br>' : ''}
|
||||
<a href="/characters/view/${char.id}" class="btn btn-sm btn-primary mt-2">View Details</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading user characters:', error);
|
||||
const container = document.getElementById('character-cards-container');
|
||||
container.innerHTML = `
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
Failed to load your characters. Please try refreshing the page.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Load character cards on page load
|
||||
document.addEventListener('DOMContentLoaded', loadUserCharacters);
|
||||
{{/is_authenticated}}
|
||||
</script>
|
||||
157
dDashboardServer/templates/layouts/base.html
Normal file
157
dDashboardServer/templates/layouts/base.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{page_title}} - DarkflameServer Dashboard</title>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
|
||||
<!-- DataTables CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<!-- Custom CSS consolidated -->
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
|
||||
{{#extra_head}}
|
||||
{{{extra_head}}}
|
||||
{{/extra_head}}
|
||||
</head>
|
||||
<body>
|
||||
{{#show_navbar}}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="bi bi-grid-3x3-gap-fill"></i>
|
||||
DarkflameServer Dashboard
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{#nav_home}} active{{/nav_home}}" href="/">
|
||||
<i class="bi bi-house-door"></i> Home
|
||||
</a>
|
||||
</li>
|
||||
{{#is_gm_3_plus}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{#nav_accounts}} active{{/nav_accounts}}" href="/accounts">
|
||||
<i class="bi bi-people"></i> Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{#nav_characters}} active{{/nav_characters}}" href="/characters">
|
||||
<i class="bi bi-person-badge"></i> Characters
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-shield-check"></i> Moderation
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/moderation/pending">Pending Pets</a></li>
|
||||
<li><a class="dropdown-item" href="/properties">Properties</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{#nav_mail}} active{{/nav_mail}}" href="/mail/send">
|
||||
<i class="bi bi-envelope"></i> Mail
|
||||
</a>
|
||||
</li>
|
||||
{{/is_gm_3_plus}}
|
||||
{{#is_gm_5_plus}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{#nav_playkeys}} active{{/nav_playkeys}}" href="/playkeys">
|
||||
<i class="bi bi-key"></i> Play Keys
|
||||
</a>
|
||||
</li>
|
||||
{{/is_gm_5_plus}}
|
||||
{{#is_gm_8_plus}}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-journal-text"></i> Logs
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/logs/activities">Activity Logs</a></li>
|
||||
<li><a class="dropdown-item" href="/logs/commands">Command Logs</a></li>
|
||||
<li><a class="dropdown-item" href="/logs/audits">Audit Logs</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{{/is_gm_8_plus}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{{#nav_bugs}} active{{/nav_bugs}}" href="/bugs">
|
||||
<i class="bi bi-bug"></i> Bug Reports
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle"></i> {{username}}
|
||||
{{#gm_level_name}}
|
||||
<span class="badge bg-primary">{{gm_level_name}}</span>
|
||||
{{/gm_level_name}}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="/about">About</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="logout(); return false;">
|
||||
<i class="bi bi-box-arrow-right"></i> Logout
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{{/show_navbar}}
|
||||
|
||||
<main class="{{#show_navbar}}container-fluid mt-4{{/show_navbar}}">
|
||||
{{#flash_messages}}
|
||||
<div class="alert alert-{{type}} alert-dismissible fade show" role="alert">
|
||||
{{message}}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{{/flash_messages}}
|
||||
|
||||
{{{content}}}
|
||||
</main>
|
||||
|
||||
<footer class="footer mt-auto py-3 bg-dark border-top">
|
||||
<div class="container text-center">
|
||||
<span class="text-muted">DarkflameServer Dashboard © 2025 | Powered by Crow C++</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
|
||||
<!-- DataTables JS -->
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<!-- Shared helper: wait for jQuery/DataTables (keeps pages resilient to CDN timing) -->
|
||||
<script src="/static/js/wait-for-jq-dt.js"></script>
|
||||
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
<script src="/static/js/login.js"></script>
|
||||
|
||||
{{#extra_scripts}}
|
||||
{{{extra_scripts}}}
|
||||
{{/extra_scripts}}
|
||||
</body>
|
||||
</html>
|
||||
31
dDashboardServer/templates/login.html
Normal file
31
dDashboardServer/templates/login.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow-lg mt-5">
|
||||
<div class="card-header bg-primary text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-shield-lock"></i>
|
||||
DarkflameServer Dashboard
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="login-form">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-box-arrow-in-right"></i>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="login-message" class="mt-3" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
73
dDashboardServer/templates/logs/activities.html
Normal file
73
dDashboardServer/templates/logs/activities.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-activity"></i>
|
||||
Activity Logs
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Player Activity</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="activity-log-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Character</th>
|
||||
<th>Activity</th>
|
||||
<th>Map ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Populated via DataTables Ajax -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize when libraries are ready
|
||||
safeInit(function($) {
|
||||
$('#activity-log-table').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
ajax: {
|
||||
url: '/api/activity-log',
|
||||
type: 'GET'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
data: 'timestamp',
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display' || type === 'filter') {
|
||||
const date = new Date(data * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'character_name',
|
||||
render: function(data, type, row) {
|
||||
return `<a href="/characters/view/${row.character_id}">${data}</a>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'activity_name'
|
||||
},
|
||||
{
|
||||
data: 'map_id'
|
||||
}
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
pageLength: 25
|
||||
});
|
||||
}, { requireApi: false, timeout: 8000 });
|
||||
</script>
|
||||
139
dDashboardServer/templates/logs/audits.html
Normal file
139
dDashboardServer/templates/logs/audits.html
Normal file
@@ -0,0 +1,139 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-journal-check"></i>
|
||||
Audit Logs
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Dashboard Audit Trail</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="audits-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Admin</th>
|
||||
<th>Action</th>
|
||||
<th>Target</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Populated via DataTables Ajax -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showLibraryError(message) {
|
||||
const el = document.getElementById('audits-table');
|
||||
if (el) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'alert alert-danger';
|
||||
wrapper.textContent = message;
|
||||
el.replaceWith(wrapper);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
function loadAuditLogs() {
|
||||
API.get('/api/auth/status').then(status => {
|
||||
if (!status || !status.authenticated || status.gm_level < 8) {
|
||||
showLibraryError('You do not have permission to view audit logs. GM Level 8+ required.');
|
||||
return;
|
||||
}
|
||||
|
||||
API.get('/api/logs/audits').then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#audits-table')) {
|
||||
const table = $('#audits-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
$('#audits-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{
|
||||
data: 'timestamp',
|
||||
render: function(d) {
|
||||
if (!d || d === 0) return '-';
|
||||
return new Date(d * 1000).toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'admin_username',
|
||||
render: function(d, t, row) {
|
||||
if (row.admin_account_id) {
|
||||
return `<a href="/accounts/view/${row.admin_account_id}">${d || row.admin_account_id}</a>`;
|
||||
}
|
||||
return d || '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'action',
|
||||
render: function(d) {
|
||||
// Color-code actions
|
||||
const badges = {
|
||||
'ban': 'danger',
|
||||
'unban': 'success',
|
||||
'lock': 'warning',
|
||||
'unlock': 'success',
|
||||
'mute': 'warning',
|
||||
'unmute': 'success',
|
||||
'delete': 'danger',
|
||||
'create': 'success',
|
||||
'update': 'info',
|
||||
'gm_level_change': 'primary'
|
||||
};
|
||||
const action = d.toLowerCase();
|
||||
const badgeClass = badges[action] || 'secondary';
|
||||
return `<span class="badge bg-${badgeClass}">${d}</span>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'target_type',
|
||||
render: function(d, t, row) {
|
||||
if (!d) return '-';
|
||||
if (d === 'account' && row.target_id) {
|
||||
return `<a href="/accounts/view/${row.target_id}">Account ${row.target_id}</a>`;
|
||||
} else if (d === 'character' && row.target_id) {
|
||||
return `<a href="/characters/view/${row.target_id}">Character ${row.target_id}</a>`;
|
||||
}
|
||||
return `${d} ${row.target_id || ''}`;
|
||||
}
|
||||
},
|
||||
{ data: 'details', render: d => d || '-' }
|
||||
],
|
||||
pageLength: 25,
|
||||
order: [[0, 'desc']],
|
||||
processing: true
|
||||
});
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
const msg = err && err.message ? err.message : 'Failed to load audit logs';
|
||||
showLibraryError(`Error loading audit logs: ${msg}`);
|
||||
});
|
||||
|
||||
}).catch(err => {
|
||||
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when jQuery/DataTables and API are ready
|
||||
safeInit(function($) {
|
||||
loadAuditLogs();
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
</script>
|
||||
106
dDashboardServer/templates/logs/commands.html
Normal file
106
dDashboardServer/templates/logs/commands.html
Normal file
@@ -0,0 +1,106 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-terminal"></i>
|
||||
Command Logs
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Recent Commands</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="commands-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Character</th>
|
||||
<th>Command</th>
|
||||
<th>Arguments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Populated via DataTables Ajax -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showLibraryError(message) {
|
||||
const el = document.getElementById('commands-table');
|
||||
if (el) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'alert alert-danger';
|
||||
wrapper.textContent = message;
|
||||
el.replaceWith(wrapper);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
function loadCommandLogs() {
|
||||
API.get('/api/auth/status').then(status => {
|
||||
if (!status || !status.authenticated || status.gm_level < 8) {
|
||||
showLibraryError('You do not have permission to view command logs. GM Level 8+ required.');
|
||||
return;
|
||||
}
|
||||
|
||||
API.get('/api/logs/commands').then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#commands-table')) {
|
||||
const table = $('#commands-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
} else {
|
||||
$('#commands-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{
|
||||
data: 'timestamp',
|
||||
render: function(d) {
|
||||
if (!d || d === 0) return '-';
|
||||
return new Date(d * 1000).toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'character_name',
|
||||
render: function(d, t, row) {
|
||||
if (row.character_id) {
|
||||
return `<a href="/characters/view/${row.character_id}">${d || row.character_id}</a>`;
|
||||
}
|
||||
return d || '-';
|
||||
}
|
||||
},
|
||||
{ data: 'command' },
|
||||
{ data: 'arguments', render: d => d || '-' }
|
||||
],
|
||||
pageLength: 25,
|
||||
order: [[0, 'desc']],
|
||||
processing: true
|
||||
});
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
const msg = err && err.message ? err.message : 'Failed to load command logs';
|
||||
showLibraryError(`Error loading command logs: ${msg}`);
|
||||
});
|
||||
|
||||
}).catch(err => {
|
||||
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when jQuery/DataTables and API are ready
|
||||
safeInit(function($) {
|
||||
loadCommandLogs();
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
</script>
|
||||
80
dDashboardServer/templates/mail/send.html
Normal file
80
dDashboardServer/templates/mail/send.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4"><i class="bi bi-envelope"></i> Send Mail</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Compose Mail</div>
|
||||
<div class="card-body">
|
||||
<form id="send-mail-form">
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" type="checkbox" id="send-to-all">
|
||||
<label class="form-check-label" for="send-to-all">Send to all characters</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Recipient Character ID (leave blank if sending to all)</label>
|
||||
<input type="number" id="recipient-id" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subject</label>
|
||||
<input type="text" id="subject" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Message</label>
|
||||
<textarea id="body" class="form-control" rows="6" required></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Attachment LOT (optional)</label>
|
||||
<input type="number" id="attachment-lot" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Attachment Count</label>
|
||||
<input type="number" id="attachment-count" class="form-control" value="1" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Send Mail</button>
|
||||
</form>
|
||||
<div id="mail-result" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('send-mail-form');
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const sendToAll = document.getElementById('send-to-all').checked;
|
||||
const recipientId = parseInt(document.getElementById('recipient-id').value) || 0;
|
||||
const subject = document.getElementById('subject').value.trim();
|
||||
const body = document.getElementById('body').value.trim();
|
||||
const lot = parseInt(document.getElementById('attachment-lot').value) || 0;
|
||||
const count = parseInt(document.getElementById('attachment-count').value) || 1;
|
||||
|
||||
const payload = { subject: subject, body: body };
|
||||
if (sendToAll) payload.send_to_all = true;
|
||||
else payload.recipient_id = recipientId;
|
||||
if (lot > 0) {
|
||||
payload.attachment_lot = lot;
|
||||
payload.attachment_count = count;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await API.post('/api/mail/send', payload);
|
||||
if (res && res.success) {
|
||||
document.getElementById('mail-result').innerHTML = `<div class="alert alert-success">Sent to ${res.recipients} recipient(s)</div>`;
|
||||
form.reset();
|
||||
} else {
|
||||
document.getElementById('mail-result').innerHTML = `<div class="alert alert-danger">${res.error || 'Failed to send mail'}</div>`;
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('mail-result').innerHTML = `<div class="alert alert-danger">${err.message}</div>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
85
dDashboardServer/templates/moderation/pets.html
Normal file
85
dDashboardServer/templates/moderation/pets.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4"><i class="bi bi-paw"></i> Pet Name Moderation</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">Pending Pet Names</div>
|
||||
<div class="card-body">
|
||||
<table id="pets-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Character</th>
|
||||
<th>Pet Name</th>
|
||||
<th>Submitted</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let petsTable = null;
|
||||
|
||||
function loadPets() {
|
||||
API.get('/api/moderation/pets').then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#pets-table')) {
|
||||
const table = $('#pets-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
petsTable = table;
|
||||
} else {
|
||||
petsTable = $('#pets-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'character_name' },
|
||||
{ data: 'pet_name' },
|
||||
{ data: 'submitted', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
|
||||
{ data: null, orderable: false, render: function(data, type, row) {
|
||||
return `
|
||||
<button class="btn btn-sm btn-success" onclick="approvePet(${row.id})">Approve</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="rejectPet(${row.id})">Reject</button>
|
||||
`;
|
||||
} }
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
pageLength: 25
|
||||
});
|
||||
}
|
||||
|
||||
}).catch(err => { alert(err && err.message ? err.message : 'Failed to load pets'); });
|
||||
}
|
||||
|
||||
// Initialize when libraries are ready
|
||||
safeInit(function($) {
|
||||
loadPets();
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
|
||||
window.approvePet = async function(id) {
|
||||
if (!confirm('Approve this pet name?')) return;
|
||||
try {
|
||||
const res = await API.post(`/api/moderation/pets/${id}/approve`);
|
||||
if (res && res.success) { loadPets(); alert('Approved'); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
};
|
||||
|
||||
window.rejectPet = async function(id) {
|
||||
if (!confirm('Reject this pet name?')) return;
|
||||
try {
|
||||
const res = await API.post(`/api/moderation/pets/${id}/reject`);
|
||||
if (res && res.success) { loadPets(); alert('Rejected'); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
};
|
||||
</script>
|
||||
82
dDashboardServer/templates/moderation/properties.html
Normal file
82
dDashboardServer/templates/moderation/properties.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4"><i class="bi bi-house"></i> Property Moderation</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">Pending Properties</div>
|
||||
<div class="card-body">
|
||||
<table id="properties-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Owner (Character)</th>
|
||||
<th>Property Name</th>
|
||||
<th>Submitted</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let propertiesTable = null;
|
||||
|
||||
function loadProperties() {
|
||||
API.get('/api/moderation/properties').then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#properties-table')) {
|
||||
const table = $('#properties-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
propertiesTable = table;
|
||||
} else {
|
||||
propertiesTable = $('#properties-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'character_name' },
|
||||
{ data: 'property_name' },
|
||||
{ data: 'submitted', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
|
||||
{ data: null, orderable: false, render: function(data, type, row) {
|
||||
return `
|
||||
<button class="btn btn-sm btn-success" onclick="approveProperty(${row.id})">Approve</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="rejectProperty(${row.id})">Reject</button>
|
||||
`;
|
||||
} }
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
pageLength: 25
|
||||
});
|
||||
}
|
||||
}).catch(err => { alert(err && err.message ? err.message : 'Failed to load properties'); });
|
||||
}
|
||||
|
||||
// Initialize when libraries are ready
|
||||
safeInit(function($) { loadProperties(); }, { requireApi: true, timeout: 8000 });
|
||||
|
||||
window.approveProperty = async function(id) {
|
||||
if (!confirm('Approve this property?')) return;
|
||||
try {
|
||||
const res = await API.post(`/api/moderation/properties/${id}/approve`);
|
||||
if (res && res.success) { loadProperties(); alert('Approved'); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
};
|
||||
|
||||
window.rejectProperty = async function(id) {
|
||||
if (!confirm('Reject this property?')) return;
|
||||
try {
|
||||
const res = await API.post(`/api/moderation/properties/${id}/reject`);
|
||||
if (res && res.success) { loadProperties(); alert('Rejected'); } else { alert(res.error || 'Failed'); }
|
||||
} catch (err) { alert(err.message); }
|
||||
};
|
||||
</script>
|
||||
155
dDashboardServer/templates/playkeys/index.html
Normal file
155
dDashboardServer/templates/playkeys/index.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4"><i class="bi bi-key"></i> Play Keys</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Create Play Keys</div>
|
||||
<div class="card-body">
|
||||
<form id="create-keys-form" class="row g-2">
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Count</label>
|
||||
<input type="number" id="key-count" class="form-control" value="1" min="1" max="100">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Uses</label>
|
||||
<input type="number" id="key-uses" class="form-control" value="1" min="1">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Notes</label>
|
||||
<input type="text" id="key-notes" class="form-control" placeholder="Optional notes">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="created-keys" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">Existing Play Keys</div>
|
||||
<div class="card-body">
|
||||
<table id="playkeys-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Key</th>
|
||||
<th>Uses</th>
|
||||
<th>Times Used</th>
|
||||
<th>Active</th>
|
||||
<th>Notes</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let playkeysTable = null;
|
||||
|
||||
function loadPlaykeys() {
|
||||
API.get('/api/playkeys').then(res => {
|
||||
const data = Array.isArray(res.data) ? res.data : (res || []);
|
||||
|
||||
if ($.fn.DataTable.isDataTable('#playkeys-table')) {
|
||||
const table = $('#playkeys-table').DataTable();
|
||||
table.clear();
|
||||
table.rows.add(data);
|
||||
table.draw(false);
|
||||
playkeysTable = table;
|
||||
} else {
|
||||
playkeysTable = $('#playkeys-table').DataTable({
|
||||
data: data,
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'key_string' },
|
||||
{ data: 'key_uses' },
|
||||
{ data: 'times_used' },
|
||||
{ data: 'active', render: d => d ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>' },
|
||||
{ data: 'notes' },
|
||||
{ data: 'created_at', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
|
||||
{ data: null, orderable: false, render: function(data, type, row) {
|
||||
return `
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteKey(${row.id})">Delete</button>
|
||||
<button class="btn btn-sm btn-info" onclick="viewKey(${row.id})">View</button>
|
||||
`;
|
||||
} }
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
pageLength: 25
|
||||
});
|
||||
|
||||
// Create keys form handler
|
||||
$('#create-keys-form').on('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const count = parseInt($('#key-count').val()) || 1;
|
||||
const uses = parseInt($('#key-uses').val()) || 1;
|
||||
const notes = $('#key-notes').val() || '';
|
||||
|
||||
try {
|
||||
const res = await API.post('/api/playkeys/create', { count: count, uses: uses, notes: notes });
|
||||
if (res && res.success) {
|
||||
$('#created-keys').html(`<div class="alert alert-success">Created ${res.count} key(s): <pre>${JSON.stringify(res.keys)}</pre></div>`);
|
||||
loadPlaykeys();
|
||||
} else {
|
||||
$('#created-keys').html(`<div class="alert alert-danger">${res.error || 'Failed to create keys'}</div>`);
|
||||
}
|
||||
} catch (err) {
|
||||
$('#created-keys').html(`<div class="alert alert-danger">${err.message}</div>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
const msg = err && err.message ? err.message : 'Failed to load play keys';
|
||||
document.getElementById('created-keys').innerHTML = `<div class="alert alert-danger">${msg}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Use safeInit to ensure jQuery/DataTables and API are present
|
||||
safeInit(function($) {
|
||||
loadPlaykeys();
|
||||
}, { requireApi: true, timeout: 8000 });
|
||||
|
||||
async function deleteKey(id) {
|
||||
if (!confirm('Delete this play key?')) return;
|
||||
try {
|
||||
const res = await API.delete(`/api/playkeys/${id}`);
|
||||
if (res && res.success) {
|
||||
loadPlaykeys();
|
||||
alert('Play key deleted');
|
||||
} else {
|
||||
alert(res.error || 'Failed to delete key');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function viewKey(id) {
|
||||
try {
|
||||
const res = await API.get(`/api/playkeys/${id}`);
|
||||
if (res && res.success) {
|
||||
const info = `ID: ${res.id}\nKey: ${res.key_string}\nUses: ${res.key_uses}\nTimes used: ${res.times_used}\nActive: ${res.active}\nNotes: ${res.notes}`;
|
||||
alert(info);
|
||||
} else {
|
||||
alert(res.error || 'Failed to get key');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
dDashboardServer/templates/register.html
Normal file
31
dDashboardServer/templates/register.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Register</div>
|
||||
<div class="card-body">
|
||||
<form id="register-form">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="play_key" class="form-label">Play Key</label>
|
||||
<input type="text" class="form-control" id="play_key" placeholder="XXXX-XXXX-XXXX-XXXX" required>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Create Account</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="register-alert" class="mt-3" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/register.js"></script>
|
||||
Reference in New Issue
Block a user