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, '&')
|
|
|
|
.replace(/</g, '<')
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
}
|
|
|
|
|
|
|
|
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>
|