Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

989 lines
45 KiB
HTML
Raw Normal View History

2025-06-13 20:30:29 +01:00
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title='Audit Dashboard', header='Audit Dashboard')}"></th:block>
<!-- Include Chart.js for visualizations -->
<script th:src="@{/js/thirdParty/chart.umd.min.js}"></script>
<style>
.dashboard-card {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stat-card {
text-align: center;
padding: 20px;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
}
.stat-label {
font-size: 1rem;
color: #666;
}
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
.filter-card {
margin-bottom: 20px;
padding: 15px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.level-indicator {
display: inline-block;
padding: 5px 10px;
border-radius: 15px;
color: white;
font-weight: bold;
}
.level-0 {
background-color: #dc3545; /* Red */
}
.level-1 {
background-color: #fd7e14; /* Orange */
}
.level-2 {
background-color: #28a745; /* Green */
}
.level-3 {
background-color: #17a2b8; /* Teal */
}
/* Custom data table styling */
.audit-table {
font-size: 0.9rem;
}
.audit-table th {
background-color: #f8f9fa;
position: sticky;
top: 0;
z-index: 10;
}
.table-responsive {
max-height: 600px;
}
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.json-viewer {
background-color: #f8f9fa;
border-radius: 4px;
padding: 10px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
}
</style>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="container-fluid mt-4">
<h1 class="mb-4">Audit Dashboard</h1>
<!-- System Status Card -->
<div class="card dashboard-card mb-4">
<div class="card-header">
<h2 class="h5 mb-0">Audit System Status</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">Status</div>
<div class="stat-number">
<span th:if="${auditEnabled}" class="text-success">Enabled</span>
<span th:unless="${auditEnabled}" class="text-danger">Disabled</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">Current Level</div>
<div class="stat-number">
<span th:class="'level-indicator level-' + ${auditLevelInt}" th:text="${auditLevel}">STANDARD</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">Retention Period</div>
<div class="stat-number" th:text="${retentionDays} + ' days'">90 days</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-label">Total Events</div>
<div class="stat-number" id="total-events">-</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabs for different sections -->
<ul class="nav nav-tabs" id="auditTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard" type="button" role="tab" aria-controls="dashboard" aria-selected="true">Dashboard</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events" type="button" role="tab" aria-controls="events" aria-selected="false">Audit Events</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="export-tab" data-bs-toggle="tab" data-bs-target="#export" type="button" role="tab" aria-controls="export" aria-selected="false">Export</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="help-tab" data-bs-toggle="tab" data-bs-target="#help" type="button" role="tab" aria-controls="help" aria-selected="false">Help</button>
</li>
</ul>
<div class="tab-content" id="auditTabsContent">
<!-- Dashboard Tab -->
<div class="tab-pane fade show active" id="dashboard" role="tabpanel" aria-labelledby="dashboard-tab">
<div class="row mt-4">
<div class="col-md-6">
<div class="card dashboard-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="h5 mb-0">Events by Type</h3>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" onclick="loadStats(7)">7 Days</button>
<button class="btn btn-sm btn-outline-secondary" onclick="loadStats(30)">30 Days</button>
<button class="btn btn-sm btn-outline-secondary" onclick="loadStats(90)">90 Days</button>
</div>
</div>
<div class="card-body">
<div class="chart-container position-relative">
<div class="loading-overlay" id="type-chart-loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<canvas id="typeChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card dashboard-card">
<div class="card-header">
<h3 class="h5 mb-0">Events by User</h3>
</div>
<div class="card-body">
<div class="chart-container position-relative">
<div class="loading-overlay" id="user-chart-loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<canvas id="userChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card dashboard-card">
<div class="card-header">
<h3 class="h5 mb-0">Events Over Time</h3>
</div>
<div class="card-body">
<div class="chart-container position-relative">
<div class="loading-overlay" id="time-chart-loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<canvas id="timeChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Events Tab -->
<div class="tab-pane fade" id="events" role="tabpanel" aria-labelledby="events-tab">
<div class="card dashboard-card mt-4">
<div class="card-header">
<h3 class="h5 mb-0">Audit Events</h3>
</div>
<div class="card-body">
<!-- Filters -->
<div class="card filter-card">
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label for="typeFilter" class="form-label">Event Type</label>
<input type="text" class="form-control" id="typeFilter" placeholder="Filter by type">
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="principalFilter" class="form-label">User</label>
<input type="text" class="form-control" id="principalFilter" placeholder="Filter by user">
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="startDateFilter" class="form-label">Start Date</label>
<input type="date" class="form-control" id="startDateFilter">
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="endDateFilter" class="form-label">End Date</label>
<input type="date" class="form-control" id="endDateFilter">
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button id="applyFilters" class="btn btn-primary">Apply Filters</button>
<button id="resetFilters" class="btn btn-secondary">Reset</button>
</div>
</div>
</div>
<!-- Event Table -->
<div class="table-responsive position-relative">
<div class="loading-overlay" id="table-loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<table class="table table-striped table-hover audit-table">
<thead>
<tr>
<th>ID</th>
<th>Time</th>
<th>User</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody id="auditTableBody">
<!-- Table rows will be populated by JavaScript -->
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="pagination-container">
<div>
<span>Show</span>
<select id="pageSizeSelect" class="form-select form-select-sm d-inline-block w-auto mx-2">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span>entries</span>
</div>
<nav aria-label="Audit events pagination">
<ul class="pagination" id="pagination">
<!-- Pagination will be populated by JavaScript -->
</ul>
</nav>
</div>
</div>
</div>
<!-- Event Details Modal -->
<div class="modal fade" id="eventDetailsModal" tabindex="-1" aria-labelledby="eventDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="eventDetailsModalLabel">Event Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-4">
<strong>ID:</strong> <span id="modal-id"></span>
</div>
<div class="col-md-4">
<strong>User:</strong> <span id="modal-principal"></span>
</div>
<div class="col-md-4">
<strong>Type:</strong> <span id="modal-type"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<strong>Time:</strong> <span id="modal-timestamp"></span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<strong>Data:</strong>
<div class="json-viewer" id="modal-data"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<!-- Export Tab -->
<div class="tab-pane fade" id="export" role="tabpanel" aria-labelledby="export-tab">
<div class="card dashboard-card mt-4">
<div class="card-header">
<h3 class="h5 mb-0">Export Audit Data</h3>
</div>
<div class="card-body">
<!-- Export Filters -->
<div class="card filter-card">
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label for="exportTypeFilter" class="form-label">Event Type</label>
<input type="text" class="form-control" id="exportTypeFilter" placeholder="Filter by type">
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="exportPrincipalFilter" class="form-label">User</label>
<input type="text" class="form-control" id="exportPrincipalFilter" placeholder="Filter by user">
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="exportStartDateFilter" class="form-label">Start Date</label>
<input type="date" class="form-control" id="exportStartDateFilter">
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="exportEndDateFilter" class="form-label">End Date</label>
<input type="date" class="form-control" id="exportEndDateFilter">
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<h5>Export Format</h5>
<div class="form-check">
<input class="form-check-input" type="radio" name="exportFormat" id="formatCSV" value="csv" checked>
<label class="form-check-label" for="formatCSV">
CSV (Comma Separated Values)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="exportFormat" id="formatJSON" value="json">
<label class="form-check-label" for="formatJSON">
JSON (JavaScript Object Notation)
</label>
</div>
</div>
<div class="col-md-6">
<button id="exportButton" class="btn btn-primary mt-4">
<i class="bi bi-download"></i> Export Data
</button>
</div>
</div>
</div>
<div class="alert alert-info mt-3">
<h5>Export Information</h5>
<p>The export will include all audit events matching the selected filters. For large datasets, the export may take a few moments to generate.</p>
<p>Exported data will include:</p>
<ul>
<li>Event ID</li>
<li>User</li>
<li>Event Type</li>
<li>Timestamp</li>
<li>Event Data</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Help Tab -->
<div class="tab-pane fade" id="help" role="tabpanel" aria-labelledby="help-tab">
<div class="card dashboard-card mt-4">
<div class="card-header">
<h3 class="h5 mb-0">Audit System Help</h3>
</div>
<div class="card-body">
<h4>About the Audit System</h4>
<p>The Stirling PDF audit system records user actions and system events for security monitoring, compliance, and troubleshooting purposes.</p>
<h4>Audit Levels</h4>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Level</th>
<th>Name</th>
<th>Description</th>
<th>Use Case</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><span class="level-indicator level-0">OFF</span></td>
<td>Minimal auditing, only critical security events</td>
<td>Development environments</td>
</tr>
<tr>
<td>1</td>
<td><span class="level-indicator level-1">BASIC</span></td>
<td>Authentication events, security events, and errors</td>
<td>Production environments with minimal storage</td>
</tr>
<tr>
<td>2</td>
<td><span class="level-indicator level-2">STANDARD</span></td>
<td>All HTTP requests and operations (default)</td>
<td>Normal production use</td>
</tr>
<tr>
<td>3</td>
<td><span class="level-indicator level-3">VERBOSE</span></td>
<td>Detailed information including headers, parameters, and results</td>
<td>Troubleshooting and detailed analysis</td>
</tr>
</tbody>
</table>
</div>
<h4>Configuration</h4>
<p>Audit settings are configured in the <code>settings.yml</code> file under the <code>premium.proFeatures.audit</code> section:</p>
<pre><code>premium:
proFeatures:
audit:
enabled: true # Enable/disable audit logging
level: 2 # Audit level (0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE)
retentionDays: 90 # Number of days to retain audit logs</code></pre>
<h4>Common Event Types</h4>
<ul>
<th:block th:each="level : ${auditLevels}">
<li th:if="${level.name() == 'BASIC'}">
<strong>BASIC Events:</strong>
<ul>
<li>USER_LOGIN - User login</li>
<li>USER_LOGOUT - User logout</li>
<li>USER_FAILED_LOGIN - Failed login attempt</li>
<li>USER_PROFILE_UPDATE - User or profile operations</li>
</ul>
</li>
<li th:if="${level.name() == 'STANDARD'}">
<strong>STANDARD Events:</strong>
<ul>
<li>HTTP_REQUEST - GET requests for viewing</li>
<li>PDF_PROCESS - PDF processing operations</li>
2025-06-14 00:14:47 +01:00
<li>FILE_OPERATION - File-related operations</li>
2025-06-13 20:30:29 +01:00
<li>SETTINGS_CHANGED - System or admin settings operations</li>
</ul>
</li>
<li th:if="${level.name() == 'VERBOSE'}">
<strong>VERBOSE Events:</strong>
<ul>
<li>Detailed versions of STANDARD events with parameters and results</li>
</ul>
</li>
</th:block>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS is loaded by the common fragments -->
<script th:src="@{/js/thirdParty/jquery.min.js}"></script>
<script th:src="@{/js/thirdParty/bootstrap.min.js}"></script>
<script>
// Initialize variables
let currentPage = 0;
let pageSize = 20;
let totalPages = 0;
let typeFilter = '';
let principalFilter = '';
let startDateFilter = '';
let endDateFilter = '';
// Charts
let typeChart;
let userChart;
let timeChart;
// DOM elements
const auditTableBody = document.getElementById('auditTableBody');
const pagination = document.getElementById('pagination');
const pageSizeSelect = document.getElementById('pageSizeSelect');
const typeFilterInput = document.getElementById('typeFilter');
const principalFilterInput = document.getElementById('principalFilter');
const startDateFilterInput = document.getElementById('startDateFilter');
const endDateFilterInput = document.getElementById('endDateFilter');
const applyFiltersButton = document.getElementById('applyFilters');
const resetFiltersButton = document.getElementById('resetFilters');
// Modal elements
const eventDetailsModal = document.getElementById('eventDetailsModal');
const modalId = document.getElementById('modal-id');
const modalPrincipal = document.getElementById('modal-principal');
const modalType = document.getElementById('modal-type');
const modalTimestamp = document.getElementById('modal-timestamp');
const modalData = document.getElementById('modal-data');
// Export elements
const exportTypeFilter = document.getElementById('exportTypeFilter');
const exportPrincipalFilter = document.getElementById('exportPrincipalFilter');
const exportStartDateFilter = document.getElementById('exportStartDateFilter');
const exportEndDateFilter = document.getElementById('exportEndDateFilter');
const exportButton = document.getElementById('exportButton');
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
// Load initial data
loadAuditData();
loadStats(7);
// Set up event listeners
pageSizeSelect.addEventListener('change', function() {
pageSize = parseInt(this.value);
currentPage = 0;
loadAuditData();
});
applyFiltersButton.addEventListener('click', function() {
typeFilter = typeFilterInput.value.trim();
principalFilter = principalFilterInput.value.trim();
startDateFilter = startDateFilterInput.value;
endDateFilter = endDateFilterInput.value;
currentPage = 0;
loadAuditData();
});
resetFiltersButton.addEventListener('click', function() {
typeFilterInput.value = '';
principalFilterInput.value = '';
startDateFilterInput.value = '';
endDateFilterInput.value = '';
typeFilter = '';
principalFilter = '';
startDateFilter = '';
endDateFilter = '';
currentPage = 0;
loadAuditData();
});
exportButton.addEventListener('click', function() {
const exportFormat = document.querySelector('input[name="exportFormat"]:checked').value;
exportAuditData(exportFormat);
});
// Set up tab change events
const tabEl = document.querySelector('button[data-bs-toggle="tab"]');
tabEl.addEventListener('shown.bs.tab', function (event) {
const targetId = event.target.getAttribute('data-bs-target');
if (targetId === '#dashboard') {
// Redraw charts when dashboard tab is shown
if (typeChart) typeChart.update();
if (userChart) userChart.update();
if (timeChart) timeChart.update();
}
});
});
// Load audit data from server
function loadAuditData() {
showLoading('table-loading');
let url = `/audit/data?page=${currentPage}&size=${pageSize}`;
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
if (startDateFilter) url += `&startDate=${startDateFilter}`;
if (endDateFilter) url += `&endDate=${endDateFilter}`;
fetch(url)
.then(response => response.json())
.then(data => {
renderTable(data.content);
renderPagination(data.totalPages, data.currentPage);
totalPages = data.totalPages;
hideLoading('table-loading');
})
.catch(error => {
console.error('Error loading audit data:', error);
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">Error loading data: ${error.message}</td></tr>`;
hideLoading('table-loading');
});
}
// Load statistics for charts
function loadStats(days) {
showLoading('type-chart-loading');
showLoading('user-chart-loading');
showLoading('time-chart-loading');
fetch(`/audit/stats?days=${days}`)
.then(response => response.json())
.then(data => {
document.getElementById('total-events').textContent = data.totalEvents;
renderCharts(data);
hideLoading('type-chart-loading');
hideLoading('user-chart-loading');
hideLoading('time-chart-loading');
})
.catch(error => {
console.error('Error loading stats:', error);
hideLoading('type-chart-loading');
hideLoading('user-chart-loading');
hideLoading('time-chart-loading');
});
}
// Export audit data
function exportAuditData(format) {
const type = exportTypeFilter.value.trim();
const principal = exportPrincipalFilter.value.trim();
const startDate = exportStartDateFilter.value;
const endDate = exportEndDateFilter.value;
let url = format === 'json' ? '/audit/export/json?' : '/audit/export?';
if (type) url += `&type=${encodeURIComponent(type)}`;
if (principal) url += `&principal=${encodeURIComponent(principal)}`;
if (startDate) url += `&startDate=${startDate}`;
if (endDate) url += `&endDate=${endDate}`;
// Trigger download
window.location.href = url;
}
// Render table with audit data
function renderTable(events) {
if (!events || events.length === 0) {
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">No audit events found</td></tr>';
return;
}
auditTableBody.innerHTML = '';
events.forEach(event => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${event.id}</td>
<td>${formatDate(event.timestamp)}</td>
<td>${escapeHtml(event.principal)}</td>
<td>${escapeHtml(event.type)}</td>
<td><button class="btn btn-sm btn-outline-primary view-details">View Details</button></td>
`;
// Store event data for modal
row.dataset.event = JSON.stringify(event);
// Add click handler for details button
row.querySelector('.view-details').addEventListener('click', function() {
showEventDetails(event);
});
auditTableBody.appendChild(row);
});
}
// Show event details in modal
function showEventDetails(event) {
modalId.textContent = event.id;
modalPrincipal.textContent = event.principal;
modalType.textContent = event.type;
modalTimestamp.textContent = formatDate(event.timestamp);
// Format JSON data
try {
const dataObj = JSON.parse(event.data);
modalData.textContent = JSON.stringify(dataObj, null, 2);
} catch (e) {
modalData.textContent = event.data;
}
// Show the modal
const modal = new bootstrap.Modal(eventDetailsModal);
modal.show();
}
// Render pagination controls
function renderPagination(totalPages, currentPage) {
pagination.innerHTML = '';
// Previous button
const prevLi = document.createElement('li');
prevLi.classList.add('page-item');
if (currentPage === 0) prevLi.classList.add('disabled');
const prevLink = document.createElement('a');
prevLink.classList.add('page-link');
prevLink.href = '#';
prevLink.textContent = 'Previous';
prevLink.addEventListener('click', function(e) {
e.preventDefault();
if (currentPage > 0) {
goToPage(currentPage - 1);
}
});
prevLi.appendChild(prevLink);
pagination.appendChild(prevLi);
// Page numbers
const maxPages = 5; // Max number of page links to show
const startPage = Math.max(0, currentPage - Math.floor(maxPages / 2));
const endPage = Math.min(totalPages - 1, startPage + maxPages - 1);
for (let i = startPage; i <= endPage; i++) {
const pageLi = document.createElement('li');
pageLi.classList.add('page-item');
if (i === currentPage) pageLi.classList.add('active');
const pageLink = document.createElement('a');
pageLink.classList.add('page-link');
pageLink.href = '#';
pageLink.textContent = i + 1;
pageLink.addEventListener('click', function(e) {
e.preventDefault();
goToPage(i);
});
pageLi.appendChild(pageLink);
pagination.appendChild(pageLi);
}
// Next button
const nextLi = document.createElement('li');
nextLi.classList.add('page-item');
if (currentPage >= totalPages - 1) nextLi.classList.add('disabled');
const nextLink = document.createElement('a');
nextLink.classList.add('page-link');
nextLink.href = '#';
nextLink.textContent = 'Next';
nextLink.addEventListener('click', function(e) {
e.preventDefault();
if (currentPage < totalPages - 1) {
goToPage(currentPage + 1);
}
});
nextLi.appendChild(nextLink);
pagination.appendChild(nextLi);
}
// Navigate to specific page
function goToPage(page) {
currentPage = page;
loadAuditData();
}
// Render charts
function renderCharts(data) {
// Prepare data for charts
const typeLabels = Object.keys(data.eventsByType);
const typeValues = Object.values(data.eventsByType);
const userLabels = Object.keys(data.eventsByPrincipal);
const userValues = Object.values(data.eventsByPrincipal);
// Sort days for time chart
const timeLabels = Object.keys(data.eventsByDay).sort();
const timeValues = timeLabels.map(day => data.eventsByDay[day] || 0);
// Type chart
if (typeChart) {
typeChart.destroy();
}
const typeCtx = document.getElementById('typeChart').getContext('2d');
typeChart = new Chart(typeCtx, {
type: 'bar',
data: {
labels: typeLabels,
datasets: [{
label: 'Events by Type',
data: typeValues,
2025-06-14 00:14:47 +01:00
backgroundColor: getChartColors(typeLabels.length),
borderColor: getChartColors(typeLabels.length, 1), // Full opacity for borders
2025-06-13 20:30:29 +01:00
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// User chart
if (userChart) {
userChart.destroy();
}
const userCtx = document.getElementById('userChart').getContext('2d');
userChart = new Chart(userCtx, {
type: 'pie',
data: {
labels: userLabels,
datasets: [{
label: 'Events by User',
data: userValues,
2025-06-14 00:14:47 +01:00
backgroundColor: getChartColors(userLabels.length),
2025-06-13 20:30:29 +01:00
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
// Time chart
if (timeChart) {
timeChart.destroy();
}
const timeCtx = document.getElementById('timeChart').getContext('2d');
timeChart = new Chart(timeCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: 'Events Over Time',
data: timeValues,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Helper functions
function formatDate(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString();
}
function escapeHtml(text) {
if (!text) return '';
return text
.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function showLoading(id) {
const loading = document.getElementById(id);
if (loading) loading.style.display = 'flex';
}
function hideLoading(id) {
const loading = document.getElementById(id);
if (loading) loading.style.display = 'none';
}
2025-06-14 00:14:47 +01:00
// Function to generate a palette of colors for charts
function getChartColors(count, opacity = 0.6) {
// Base colors - a larger palette than the default
const colors = [
[54, 162, 235], // blue
[255, 99, 132], // red
[75, 192, 192], // teal
[255, 206, 86], // yellow
[153, 102, 255], // purple
[255, 159, 64], // orange
[46, 204, 113], // green
[231, 76, 60], // dark red
[52, 152, 219], // light blue
[155, 89, 182], // violet
[241, 196, 15], // dark yellow
[26, 188, 156], // turquoise
[230, 126, 34], // dark orange
[149, 165, 166], // light gray
[243, 156, 18], // amber
[39, 174, 96], // emerald
[211, 84, 0], // dark orange red
[22, 160, 133], // green sea
[41, 128, 185], // belize hole
[142, 68, 173] // wisteria
];
const result = [];
// Always use the same format regardless of color source
if (count > colors.length) {
// Generate colors algorithmically for large sets
for (let i = 0; i < count; i++) {
// Generate a color based on position in the hue circle (0-360)
const hue = (i * 360 / count) % 360;
const sat = 70 + Math.random() * 10; // 70-80%
const light = 50 + Math.random() * 10; // 50-60%
result.push(`hsla(${hue}, ${sat}%, ${light}%, ${opacity})`);
}
} else {
// Use colors from our palette but also return in hsla format for consistency
for (let i = 0; i < count; i++) {
const color = colors[i % colors.length];
result.push(`rgba(${color[0]}, ${color[1]}, ${color[2]}, ${opacity})`);
}
}
return result;
}
2025-06-13 20:30:29 +01:00
</script>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>