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>
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
<!-- Include custom CSS -->
|
|
|
|
<link rel="stylesheet" th:href="@{/css/audit-dashboard.css}" />
|
2025-06-13 20:30:29 +01:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="page-container">
|
|
|
|
<div id="content-wrap">
|
2025-06-15 01:15:12 +01:00
|
|
|
<!-- DO NOT REMOVE <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> -->
|
2025-06-13 20:30:29 +01:00
|
|
|
|
|
|
|
<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>
|
2025-06-15 01:15:12 +01:00
|
|
|
<select class="form-select" id="typeFilter">
|
|
|
|
<option value="">All event types</option>
|
|
|
|
<!-- Will be populated from API -->
|
|
|
|
</select>
|
2025-06-13 20:30:29 +01:00
|
|
|
</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>
|
2025-06-15 01:15:12 +01:00
|
|
|
<span class="mx-3">Page <span id="currentPage">1</span> of <span id="totalPages">1</span> (Total records: <span id="totalRecords">0</span>)</span>
|
2025-06-13 20:30:29 +01:00
|
|
|
</div>
|
|
|
|
<nav aria-label="Audit events pagination">
|
2025-06-15 01:15:12 +01:00
|
|
|
<div class="btn-group" role="group" aria-label="Pagination">
|
|
|
|
<button type="button" class="btn btn-outline-primary" id="page-first">«</button>
|
|
|
|
<button type="button" class="btn btn-outline-primary" id="page-prev">‹</button>
|
|
|
|
<span class="btn btn-outline-secondary disabled" id="page-indicator">Page 1 of 1</span>
|
|
|
|
<button type="button" class="btn btn-outline-primary" id="page-next">›</button>
|
|
|
|
<button type="button" class="btn btn-outline-primary" id="page-last">»</button>
|
|
|
|
</div>
|
2025-06-13 20:30:29 +01:00
|
|
|
</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>
|
2025-06-15 01:15:12 +01:00
|
|
|
<select class="form-select" id="exportTypeFilter">
|
|
|
|
<option value="">All event types</option>
|
|
|
|
<!-- Will be populated from API -->
|
|
|
|
</select>
|
2025-06-13 20:30:29 +01:00
|
|
|
</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>
|
2025-06-15 01:15:12 +01:00
|
|
|
<div>
|
|
|
|
<label class="btn btn-outline-primary" style="margin-right: 10px;">
|
|
|
|
<input type="radio" name="exportFormat" id="formatCSV" value="csv" checked style="margin-right: 5px;">
|
2025-06-13 20:30:29 +01:00
|
|
|
CSV (Comma Separated Values)
|
|
|
|
</label>
|
2025-06-15 01:15:12 +01:00
|
|
|
<label class="btn btn-outline-primary">
|
|
|
|
<input type="radio" name="exportFormat" id="formatJSON" value="json" style="margin-right: 5px;">
|
2025-06-13 20:30:29 +01:00
|
|
|
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>
|
2025-06-15 01:15:12 +01:00
|
|
|
<button id="resetExportFilters" class="btn btn-secondary mt-4 ms-2">
|
|
|
|
Reset Filters
|
|
|
|
</button>
|
2025-06-13 20:30:29 +01:00
|
|
|
</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>
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
<!-- Debug console for development purposes -->
|
|
|
|
<div id="debug-console"></div>
|
|
|
|
|
|
|
|
<!-- Load custom JavaScript -->
|
|
|
|
<script th:src="@{/js/audit/dashboard.js}"></script>
|
|
|
|
|
2025-06-13 20:30:29 +01:00
|
|
|
<script>
|
2025-06-15 01:15:12 +01:00
|
|
|
// DEPRECATED - KEPT FOR REFERENCE - USE dashboard.js INSTEAD
|
2025-06-13 20:30:29 +01:00
|
|
|
// 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;
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// DOM elements - will properly initialize these during page load
|
|
|
|
let auditTableBody;
|
|
|
|
let pagination;
|
|
|
|
let pageSizeSelect;
|
|
|
|
let typeFilterInput;
|
|
|
|
let exportTypeFilterInput;
|
|
|
|
let principalFilterInput;
|
|
|
|
let startDateFilterInput;
|
|
|
|
let endDateFilterInput;
|
|
|
|
let applyFiltersButton;
|
|
|
|
let resetFiltersButton;
|
2025-06-13 20:30:29 +01:00
|
|
|
|
|
|
|
// 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');
|
2025-06-15 01:15:12 +01:00
|
|
|
const formatRadios = document.querySelectorAll('input[name="exportFormat"]');
|
|
|
|
|
|
|
|
// Debug logger function
|
|
|
|
let debugEnabled = true;
|
|
|
|
function debugLog(message, data) {
|
|
|
|
if (!debugEnabled) return;
|
|
|
|
|
|
|
|
const console = document.getElementById('debug-console');
|
|
|
|
if (console) {
|
|
|
|
if (console.style.display === 'none' || !console.style.display) {
|
|
|
|
console.style.display = 'block';
|
|
|
|
}
|
|
|
|
|
|
|
|
const time = new Date().toLocaleTimeString();
|
|
|
|
let logMessage = `[${time}] ${message}`;
|
|
|
|
|
|
|
|
if (data !== undefined) {
|
|
|
|
if (typeof data === 'object') {
|
|
|
|
try {
|
|
|
|
logMessage += ': ' + JSON.stringify(data);
|
|
|
|
} catch (e) {
|
|
|
|
logMessage += ': ' + data;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logMessage += ': ' + data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const logLine = document.createElement('div');
|
|
|
|
logLine.textContent = logMessage;
|
|
|
|
console.appendChild(logLine);
|
|
|
|
console.scrollTop = console.scrollHeight;
|
|
|
|
|
|
|
|
// Keep only last 100 lines
|
|
|
|
while (console.childNodes.length > 100) {
|
|
|
|
console.removeChild(console.firstChild);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Also log to browser console
|
|
|
|
if (data !== undefined) {
|
|
|
|
window.console.log(message, data);
|
|
|
|
} else {
|
|
|
|
window.console.log(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add keyboard shortcut to toggle debug console
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
|
|
if (e.key === 'F12' && e.ctrlKey) {
|
|
|
|
const console = document.getElementById('debug-console');
|
|
|
|
if (console) {
|
|
|
|
console.style.display = console.style.display === 'none' || !console.style.display ? 'block' : 'none';
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2025-06-13 20:30:29 +01:00
|
|
|
|
|
|
|
// Initialize page
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('Page initialized');
|
|
|
|
|
|
|
|
// Initialize DOM references
|
|
|
|
auditTableBody = document.getElementById('auditTableBody');
|
|
|
|
pageSizeSelect = document.getElementById('pageSizeSelect');
|
|
|
|
typeFilterInput = document.getElementById('typeFilter');
|
|
|
|
exportTypeFilterInput = document.getElementById('exportTypeFilter');
|
|
|
|
principalFilterInput = document.getElementById('principalFilter');
|
|
|
|
startDateFilterInput = document.getElementById('startDateFilter');
|
|
|
|
endDateFilterInput = document.getElementById('endDateFilter');
|
|
|
|
applyFiltersButton = document.getElementById('applyFilters');
|
|
|
|
resetFiltersButton = document.getElementById('resetFilters');
|
|
|
|
|
|
|
|
// Debug log DOM elements
|
|
|
|
debugLog('DOM elements initialized', {
|
|
|
|
auditTableBody: !!auditTableBody,
|
|
|
|
pagination: !!pagination,
|
|
|
|
pageSizeSelect: !!pageSizeSelect
|
|
|
|
});
|
|
|
|
|
|
|
|
// Load event types for dropdowns
|
|
|
|
loadEventTypes();
|
|
|
|
|
|
|
|
// Show a loading message immediately
|
|
|
|
if (auditTableBody) {
|
|
|
|
auditTableBody.innerHTML =
|
|
|
|
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> Loading audit data...</td></tr>';
|
|
|
|
} else {
|
|
|
|
debugLog('ERROR: auditTableBody element not found!');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make a direct API call first to avoid validation issues
|
|
|
|
loadAuditData(0, pageSize);
|
|
|
|
|
|
|
|
// Load statistics for dashboard
|
2025-06-13 20:30:29 +01:00
|
|
|
loadStats(7);
|
|
|
|
|
|
|
|
// Set up event listeners
|
|
|
|
pageSizeSelect.addEventListener('change', function() {
|
|
|
|
pageSize = parseInt(this.value);
|
2025-06-15 01:15:12 +01:00
|
|
|
window.originalPageSize = pageSize;
|
2025-06-13 20:30:29 +01:00
|
|
|
currentPage = 0;
|
2025-06-15 01:15:12 +01:00
|
|
|
window.requestedPage = 0;
|
|
|
|
loadAuditData(0, pageSize);
|
2025-06-13 20:30:29 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
applyFiltersButton.addEventListener('click', function() {
|
|
|
|
typeFilter = typeFilterInput.value.trim();
|
|
|
|
principalFilter = principalFilterInput.value.trim();
|
|
|
|
startDateFilter = startDateFilterInput.value;
|
|
|
|
endDateFilter = endDateFilterInput.value;
|
|
|
|
currentPage = 0;
|
2025-06-15 01:15:12 +01:00
|
|
|
window.requestedPage = 0;
|
|
|
|
debugLog('Applying filters and resetting to page 0');
|
|
|
|
loadAuditData(0, pageSize);
|
2025-06-13 20:30:29 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
resetFiltersButton.addEventListener('click', function() {
|
2025-06-15 01:15:12 +01:00
|
|
|
// Reset input fields
|
2025-06-13 20:30:29 +01:00
|
|
|
typeFilterInput.value = '';
|
|
|
|
principalFilterInput.value = '';
|
|
|
|
startDateFilterInput.value = '';
|
|
|
|
endDateFilterInput.value = '';
|
2025-06-15 01:15:12 +01:00
|
|
|
|
|
|
|
// Reset filter variables
|
2025-06-13 20:30:29 +01:00
|
|
|
typeFilter = '';
|
|
|
|
principalFilter = '';
|
|
|
|
startDateFilter = '';
|
|
|
|
endDateFilter = '';
|
2025-06-15 01:15:12 +01:00
|
|
|
|
|
|
|
// Reset page
|
2025-06-13 20:30:29 +01:00
|
|
|
currentPage = 0;
|
2025-06-15 01:15:12 +01:00
|
|
|
window.requestedPage = 0;
|
|
|
|
|
|
|
|
// Update UI
|
|
|
|
document.getElementById('currentPage').textContent = '1';
|
|
|
|
|
|
|
|
debugLog('Resetting filters and going to page 0');
|
|
|
|
|
|
|
|
// Load data with reset filters
|
|
|
|
loadAuditData(0, pageSize);
|
2025-06-13 20:30:29 +01:00
|
|
|
});
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Reset export filters button
|
|
|
|
document.getElementById('resetExportFilters').addEventListener('click', function() {
|
|
|
|
exportTypeFilter.value = '';
|
|
|
|
exportPrincipalFilter.value = '';
|
|
|
|
exportStartDateFilter.value = '';
|
|
|
|
exportEndDateFilter.value = '';
|
2025-06-13 20:30:29 +01:00
|
|
|
});
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Make radio buttons behave like toggle buttons
|
|
|
|
const radioLabels = document.querySelectorAll('label.btn-outline-primary');
|
|
|
|
radioLabels.forEach(label => {
|
|
|
|
const radio = label.querySelector('input[type="radio"]');
|
|
|
|
|
|
|
|
if (radio) {
|
|
|
|
// Highlight the checked radio button's label
|
|
|
|
if (radio.checked) {
|
|
|
|
label.classList.add('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle clicking on the label
|
|
|
|
label.addEventListener('click', function() {
|
|
|
|
// Remove active class from all labels
|
|
|
|
radioLabels.forEach(l => l.classList.remove('active'));
|
|
|
|
|
|
|
|
// Add active class to this label
|
|
|
|
this.classList.add('active');
|
|
|
|
|
|
|
|
// Check this radio button
|
|
|
|
radio.checked = true;
|
|
|
|
|
|
|
|
debugLog('Radio format selected', radio.value);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Handle export button with debug
|
|
|
|
exportButton.onclick = function(e) {
|
|
|
|
debugLog('Export button clicked');
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
// Get selected format with fallback
|
|
|
|
const selectedRadio = document.querySelector('input[name="exportFormat"]:checked');
|
|
|
|
const exportFormat = selectedRadio ? selectedRadio.value : 'csv';
|
|
|
|
|
|
|
|
debugLog('Selected format', exportFormat);
|
|
|
|
exportAuditData(exportFormat);
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set up pagination buttons
|
|
|
|
document.getElementById('page-first').onclick = function() {
|
|
|
|
debugLog('First page button clicked');
|
|
|
|
if (currentPage > 0) {
|
|
|
|
goToPage(0);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
document.getElementById('page-prev').onclick = function() {
|
|
|
|
debugLog('Previous page button clicked');
|
|
|
|
if (currentPage > 0) {
|
|
|
|
goToPage(currentPage - 1);
|
2025-06-13 20:30:29 +01:00
|
|
|
}
|
2025-06-15 01:15:12 +01:00
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
document.getElementById('page-next').onclick = function() {
|
|
|
|
debugLog('Next page button clicked');
|
|
|
|
if (currentPage < totalPages - 1) {
|
|
|
|
goToPage(currentPage + 1);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
document.getElementById('page-last').onclick = function() {
|
|
|
|
debugLog('Last page button clicked');
|
|
|
|
if (totalPages > 0 && currentPage < totalPages - 1) {
|
|
|
|
goToPage(totalPages - 1);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set up tab change events
|
|
|
|
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]');
|
|
|
|
tabEls.forEach(tabEl => {
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
});
|
2025-06-13 20:30:29 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Load audit data from server - completely rewritten with client-side pagination
|
|
|
|
function loadAuditData(targetPage, originalPageSize) {
|
|
|
|
const requestedPage = targetPage !== undefined ? targetPage : window.requestedPage || 0;
|
|
|
|
const realPageSize = originalPageSize || pageSize;
|
|
|
|
|
|
|
|
debugLog('Loading audit data', {
|
|
|
|
currentPage: currentPage,
|
|
|
|
requestedPage: requestedPage,
|
|
|
|
pageSize: pageSize,
|
|
|
|
realPageSize: realPageSize
|
|
|
|
});
|
|
|
|
|
2025-06-13 20:30:29 +01:00
|
|
|
showLoading('table-loading');
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Always request page 0 from server, but with increased page size if needed
|
|
|
|
let url = `/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
2025-06-13 20:30:29 +01:00
|
|
|
|
|
|
|
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
|
|
|
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
|
|
|
if (startDateFilter) url += `&startDate=${startDateFilter}`;
|
|
|
|
if (endDateFilter) url += `&endDate=${endDateFilter}`;
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('Fetching URL', url);
|
|
|
|
|
|
|
|
// Update page indicator immediately for better UX
|
|
|
|
document.getElementById('page-indicator').textContent = `Page ${requestedPage + 1} of ?`;
|
|
|
|
|
2025-06-13 20:30:29 +01:00
|
|
|
fetch(url)
|
2025-06-15 01:15:12 +01:00
|
|
|
.then(response => {
|
|
|
|
debugLog('Response received', response.status);
|
|
|
|
return response.json();
|
|
|
|
})
|
2025-06-13 20:30:29 +01:00
|
|
|
.then(data => {
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('Data received', {
|
|
|
|
totalPages: data.totalPages,
|
|
|
|
serverPage: data.currentPage,
|
|
|
|
totalElements: data.totalElements,
|
|
|
|
contentSize: data.content.length
|
|
|
|
});
|
|
|
|
|
|
|
|
// Calculate the correct slice of data to show for the requested page
|
|
|
|
let displayContent = data.content;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Render the correct slice of data
|
|
|
|
renderTable(displayContent);
|
|
|
|
|
|
|
|
// Calculate total pages based on the actual total elements
|
|
|
|
const calculatedTotalPages = Math.ceil(data.totalElements / realPageSize);
|
|
|
|
totalPages = calculatedTotalPages;
|
|
|
|
currentPage = requestedPage; // Use our tracked page, not server's
|
|
|
|
|
|
|
|
debugLog('Pagination state updated', {
|
|
|
|
totalPages: totalPages,
|
|
|
|
currentPage: currentPage
|
|
|
|
});
|
|
|
|
|
|
|
|
// Update UI
|
|
|
|
document.getElementById('currentPage').textContent = currentPage + 1;
|
|
|
|
document.getElementById('totalPages').textContent = totalPages;
|
|
|
|
document.getElementById('totalRecords').textContent = data.totalElements;
|
|
|
|
document.getElementById('page-indicator').textContent = `Page ${currentPage + 1} of ${totalPages}`;
|
|
|
|
|
|
|
|
// Re-enable buttons with correct state
|
|
|
|
document.getElementById('page-first').disabled = currentPage === 0;
|
|
|
|
document.getElementById('page-prev').disabled = currentPage === 0;
|
|
|
|
document.getElementById('page-next').disabled = currentPage >= totalPages - 1;
|
|
|
|
document.getElementById('page-last').disabled = currentPage >= totalPages - 1;
|
|
|
|
|
2025-06-13 20:30:29 +01:00
|
|
|
hideLoading('table-loading');
|
2025-06-15 01:15:12 +01:00
|
|
|
|
|
|
|
// Restore original page size for next operations
|
|
|
|
if (originalPageSize) {
|
|
|
|
pageSize = originalPageSize;
|
|
|
|
debugLog('Restored original page size', pageSize);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store original page size for recovery
|
|
|
|
window.originalPageSize = realPageSize;
|
|
|
|
|
|
|
|
// Clear busy flag
|
|
|
|
window.paginationBusy = false;
|
|
|
|
debugLog('Pagination completed successfully');
|
2025-06-13 20:30:29 +01:00
|
|
|
})
|
|
|
|
.catch(error => {
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('Error loading data', error.message);
|
2025-06-13 20:30:29 +01:00
|
|
|
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">Error loading data: ${error.message}</td></tr>`;
|
|
|
|
hideLoading('table-loading');
|
2025-06-15 01:15:12 +01:00
|
|
|
|
|
|
|
// Re-enable buttons
|
|
|
|
document.getElementById('page-first').disabled = false;
|
|
|
|
document.getElementById('page-prev').disabled = false;
|
|
|
|
document.getElementById('page-next').disabled = false;
|
|
|
|
document.getElementById('page-last').disabled = false;
|
|
|
|
|
|
|
|
// Clear busy flag
|
|
|
|
window.paginationBusy = false;
|
2025-06-13 20:30:29 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('renderTable called with', events ? events.length : 0, 'events');
|
|
|
|
|
2025-06-13 20:30:29 +01:00
|
|
|
if (!events || events.length === 0) {
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('No events to render');
|
|
|
|
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">No audit events found matching the current filters</td></tr>';
|
2025-06-13 20:30:29 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
try {
|
|
|
|
debugLog('Clearing table body');
|
|
|
|
auditTableBody.innerHTML = '';
|
2025-06-13 20:30:29 +01:00
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('Processing events for table');
|
|
|
|
events.forEach((event, index) => {
|
|
|
|
try {
|
|
|
|
const row = document.createElement('tr');
|
|
|
|
row.innerHTML = `
|
|
|
|
<td>${event.id || 'N/A'}</td>
|
|
|
|
<td>${formatDate(event.timestamp)}</td>
|
|
|
|
<td>${escapeHtml(event.principal || 'N/A')}</td>
|
|
|
|
<td>${escapeHtml(event.type || 'N/A')}</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
|
|
|
|
const detailsButton = row.querySelector('.view-details');
|
|
|
|
if (detailsButton) {
|
|
|
|
detailsButton.addEventListener('click', function() {
|
|
|
|
showEventDetails(event);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
auditTableBody.appendChild(row);
|
|
|
|
} catch (rowError) {
|
|
|
|
debugLog('Error rendering row ' + index, rowError.message);
|
|
|
|
}
|
2025-06-13 20:30:29 +01:00
|
|
|
});
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
debugLog('Table rendering complete');
|
|
|
|
} catch (e) {
|
|
|
|
debugLog('Error in renderTable', e.message);
|
|
|
|
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error rendering table: ' + e.message + '</td></tr>';
|
|
|
|
}
|
2025-06-13 20:30:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// No need for a dynamic pagination renderer anymore as we're using static buttons
|
|
|
|
|
|
|
|
// Direct pagination approach - server seems to be hard-limited to returning 20 items
|
|
|
|
function goToPage(page) {
|
|
|
|
debugLog('goToPage called with page', page);
|
2025-06-13 20:30:29 +01:00
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Basic validation - totalPages may not be initialized on first load
|
|
|
|
if (page < 0) {
|
|
|
|
debugLog('Invalid page', page);
|
|
|
|
return;
|
|
|
|
}
|
2025-06-13 20:30:29 +01:00
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Skip validation against totalPages on first load
|
|
|
|
if (totalPages > 0 && page >= totalPages) {
|
|
|
|
debugLog('Page exceeds total pages', page);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Simple guard flag
|
|
|
|
if (window.paginationBusy) {
|
|
|
|
debugLog('Pagination busy, ignoring request');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
window.paginationBusy = true;
|
2025-06-13 20:30:29 +01:00
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
try {
|
|
|
|
debugLog('Setting page to', page);
|
2025-06-13 20:30:29 +01:00
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Store the requested page for later
|
|
|
|
window.requestedPage = page;
|
|
|
|
currentPage = page;
|
|
|
|
|
|
|
|
// Update UI immediately for user feedback
|
|
|
|
document.getElementById('currentPage').textContent = page + 1;
|
|
|
|
|
|
|
|
// Show loading state
|
|
|
|
document.getElementById('auditTableBody').innerHTML =
|
|
|
|
'<tr><td colspan="5" class="text-center">Loading page ' + (page + 1) + '...</td></tr>';
|
|
|
|
|
|
|
|
// Disable pagination buttons during load
|
|
|
|
document.getElementById('page-first').disabled = true;
|
|
|
|
document.getElementById('page-prev').disabled = true;
|
|
|
|
document.getElementById('page-next').disabled = true;
|
|
|
|
document.getElementById('page-last').disabled = true;
|
|
|
|
|
|
|
|
// Request the specific page directly
|
|
|
|
debugLog('Directly requesting server page', page);
|
|
|
|
|
|
|
|
// Force a new API call with the requested page
|
|
|
|
let apiUrl = `/audit/data?page=${page}&size=${pageSize}`;
|
|
|
|
|
|
|
|
if (typeFilter) apiUrl += `&type=${encodeURIComponent(typeFilter)}`;
|
|
|
|
if (principalFilter) apiUrl += `&principal=${encodeURIComponent(principalFilter)}`;
|
|
|
|
if (startDateFilter) apiUrl += `&startDate=${startDateFilter}`;
|
|
|
|
if (endDateFilter) apiUrl += `&endDate=${endDateFilter}`;
|
|
|
|
|
|
|
|
debugLog('Making direct API call to', apiUrl);
|
2025-06-13 20:30:29 +01:00
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Directly make the fetch here instead of using loadAuditData
|
|
|
|
fetch(apiUrl)
|
|
|
|
.then(response => {
|
|
|
|
debugLog('Response received for page ' + page, response.status);
|
|
|
|
return response.json();
|
|
|
|
})
|
|
|
|
.then(data => {
|
|
|
|
debugLog('Data received for page ' + page, {
|
|
|
|
totalPages: data.totalPages,
|
|
|
|
serverPage: data.currentPage,
|
|
|
|
totalElements: data.totalElements,
|
|
|
|
contentSize: data.content.length
|
|
|
|
});
|
|
|
|
|
|
|
|
// Render the data directly
|
|
|
|
renderTable(data.content);
|
|
|
|
|
|
|
|
// Update pagination state
|
|
|
|
totalPages = data.totalPages;
|
|
|
|
|
|
|
|
// Update UI
|
|
|
|
document.getElementById('currentPage').textContent = page + 1;
|
|
|
|
document.getElementById('totalPages').textContent = totalPages;
|
|
|
|
document.getElementById('totalRecords').textContent = data.totalElements;
|
|
|
|
document.getElementById('page-indicator').textContent = `Page ${page + 1} of ${totalPages}`;
|
|
|
|
|
|
|
|
// Re-enable buttons with correct state
|
|
|
|
document.getElementById('page-first').disabled = page === 0;
|
|
|
|
document.getElementById('page-prev').disabled = page === 0;
|
|
|
|
document.getElementById('page-next').disabled = page >= totalPages - 1;
|
|
|
|
document.getElementById('page-last').disabled = page >= totalPages - 1;
|
|
|
|
|
|
|
|
hideLoading('table-loading');
|
|
|
|
|
|
|
|
// Clear busy flag
|
|
|
|
window.paginationBusy = false;
|
|
|
|
debugLog('Direct pagination completed successfully');
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
debugLog('Error in direct pagination', error.message);
|
|
|
|
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">Error loading page ${page + 1}: ${error.message}</td></tr>`;
|
|
|
|
hideLoading('table-loading');
|
|
|
|
window.paginationBusy = false;
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
debugLog('Error in pagination', e.message);
|
|
|
|
window.paginationBusy = false;
|
2025-06-13 20:30:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
2025-06-15 01:15:12 +01:00
|
|
|
// Load event types from the server for filter dropdowns
|
|
|
|
function loadEventTypes() {
|
|
|
|
fetch('/audit/types')
|
|
|
|
.then(response => response.json())
|
|
|
|
.then(types => {
|
|
|
|
if (!types || types.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate the type filter dropdowns
|
|
|
|
const typeFilter = document.getElementById('typeFilter');
|
|
|
|
const exportTypeFilter = document.getElementById('exportTypeFilter');
|
|
|
|
|
|
|
|
// Clear existing options except the first one (All event types)
|
|
|
|
while (typeFilter.options.length > 1) {
|
|
|
|
typeFilter.remove(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
while (exportTypeFilter.options.length > 1) {
|
|
|
|
exportTypeFilter.remove(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add new options
|
|
|
|
types.forEach(type => {
|
|
|
|
// Main filter dropdown
|
|
|
|
const option = document.createElement('option');
|
|
|
|
option.value = type;
|
|
|
|
option.textContent = type;
|
|
|
|
typeFilter.appendChild(option);
|
|
|
|
|
|
|
|
// Export filter dropdown
|
|
|
|
const exportOption = document.createElement('option');
|
|
|
|
exportOption.value = type;
|
|
|
|
exportOption.textContent = type;
|
|
|
|
exportTypeFilter.appendChild(exportOption);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
console.error('Error loading event types:', error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
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>
|
2025-06-15 01:15:12 +01:00
|
|
|
<!-- End of deprecated script -->
|
2025-06-13 20:30:29 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
2025-06-15 01:15:12 +01:00
|
|
|
<!-- DO NOT REMOVE <th:block th:insert="~{fragments/footer.html :: footer}"></th:block> -->
|
2025-06-13 20:30:29 +01:00
|
|
|
</div>
|
2025-06-15 01:15:12 +01:00
|
|
|
|
|
|
|
<!-- Debug console -->
|
|
|
|
<div id="debug-console"></div>
|
2025-06-13 20:30:29 +01:00
|
|
|
</body>
|
|
|
|
</html>
|