This commit is contained in:
Anthony Stirling 2025-06-16 00:10:10 +01:00
parent e0116106c1
commit 43c5a1970f
5 changed files with 583 additions and 1010 deletions

View File

@ -2,6 +2,19 @@
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface);
border: 1px solid var(--md-sys-color-outline-variant);
}
.card-header {
background-color: var(--md-sys-color-surface-container-high);
color: var(--md-sys-color-on-surface);
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.card-body {
background-color: var(--md-sys-color-surface-container);
} }
.stat-card { .stat-card {
text-align: center; text-align: center;
@ -13,7 +26,7 @@
} }
.stat-label { .stat-label {
font-size: 1rem; font-size: 1rem;
color: #666; color: var(--md-sys-color-on-surface-variant);
} }
.chart-container { .chart-container {
position: relative; position: relative;
@ -23,6 +36,9 @@
.filter-card { .filter-card {
margin-bottom: 20px; margin-bottom: 20px;
padding: 15px; padding: 15px;
background-color: var(--md-sys-color-surface-container-low);
border: 1px solid var(--md-sys-color-outline-variant);
border-radius: 4px;
} }
.loading-overlay { .loading-overlay {
position: absolute; position: absolute;
@ -30,7 +46,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(255, 255, 255, 0.7); background-color: var(--md-sys-color-surface-container-high, rgba(229, 232, 241, 0.8));
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -44,26 +60,42 @@
font-weight: bold; font-weight: bold;
} }
.level-0 { .level-0 {
background-color: #dc3545; /* Red */ background-color: var(--md-sys-color-error, #dc3545); /* Red */
} }
.level-1 { .level-1 {
background-color: #fd7e14; /* Orange */ background-color: var(--md-sys-color-secondary, #fd7e14); /* Orange */
} }
.level-2 { .level-2 {
background-color: #28a745; /* Green */ background-color: var(--md-nav-section-color-other, #28a745); /* Green */
} }
.level-3 { .level-3 {
background-color: #17a2b8; /* Teal */ background-color: var(--md-sys-color-tertiary, #17a2b8); /* Teal */
} }
/* Custom data table styling */ /* Custom data table styling */
.audit-table { .audit-table {
font-size: 0.9rem; font-size: 0.9rem;
color: var(--md-sys-color-on-surface);
border-color: var(--md-sys-color-outline-variant);
}
.audit-table tbody tr {
background-color: var(--md-sys-color-surface-container-low);
}
.audit-table tbody tr:nth-child(even) {
background-color: var(--md-sys-color-surface-container);
}
.audit-table tbody tr:hover {
background-color: var(--md-sys-color-surface-container-high);
} }
.audit-table th { .audit-table th {
background-color: #f8f9fa; background-color: var(--md-sys-color-surface-container-high);
color: var(--md-sys-color-on-surface);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
font-weight: bold;
} }
.table-responsive { .table-responsive {
max-height: 600px; max-height: 600px;
@ -74,7 +106,8 @@
align-items: center; align-items: center;
margin-top: 15px; margin-top: 15px;
padding: 10px 0; padding: 10px 0;
border-top: 1px solid #dee2e6; border-top: 1px solid var(--md-sys-color-outline-variant);
color: var(--md-sys-color-on-surface);
} }
.pagination .page-item.active .page-link { .pagination .page-item.active .page-link {
@ -93,13 +126,15 @@
background-color: var(--bs-light); background-color: var(--bs-light);
} }
.json-viewer { .json-viewer {
background-color: #f8f9fa; background-color: var(--md-sys-color-surface-container-low);
color: var(--md-sys-color-on-surface);
border-radius: 4px; border-radius: 4px;
padding: 10px; padding: 10px;
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
font-family: monospace; font-family: monospace;
white-space: pre-wrap; white-space: pre-wrap;
border: 1px solid var(--md-sys-color-outline-variant);
} }
/* Simple, minimal radio styling - no extras */ /* Simple, minimal radio styling - no extras */
@ -113,14 +148,14 @@
right: 0; right: 0;
width: 400px; width: 400px;
height: 200px; height: 200px;
background: rgba(0,0,0,0.8); background: var(--md-sys-color-surface-container-highest, rgba(0,0,0,0.8));
color: #0f0; color: var(--md-sys-color-tertiary, #0f0);
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
z-index: 9999; z-index: 9999;
overflow-y: auto; overflow-y: auto;
padding: 10px; padding: 10px;
border: 1px solid #0f0; border: 1px solid var(--md-sys-color-outline);
display: none; /* Changed to none by default, enable with key command */ display: none; /* Changed to none by default, enable with key command */
} }
@ -128,13 +163,64 @@
label.btn-outline-primary { label.btn-outline-primary {
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
border-color: var(--md-sys-color-primary);
color: var(--md-sys-color-primary);
} }
label.btn-outline-primary.active { label.btn-outline-primary.active {
background-color: var(--bs-primary); background-color: var(--md-sys-color-primary);
color: white; color: var(--md-sys-color-on-primary);
border-color: var(--md-sys-color-primary);
} }
label.btn-outline-primary input[type="radio"] { label.btn-outline-primary input[type="radio"] {
cursor: pointer; cursor: pointer;
} }
/* Modal overrides for dark mode */
.modal-content {
background-color: var(--md-sys-color-surface-container);
color: var(--md-sys-color-on-surface);
border-color: var(--md-sys-color-outline);
}
.modal-header {
border-bottom-color: var(--md-sys-color-outline-variant);
}
.modal-footer {
border-top-color: var(--md-sys-color-outline-variant);
}
/* Button overrides for theme consistency */
.btn-outline-primary {
color: var(--md-sys-color-primary);
border-color: var(--md-sys-color-primary);
}
.btn-outline-primary:hover {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
}
.btn-outline-secondary {
color: var(--md-sys-color-secondary);
border-color: var(--md-sys-color-secondary);
}
.btn-outline-secondary:hover {
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
}
.btn-primary {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-color: var(--md-sys-color-primary);
}
.btn-secondary {
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
border-color: var(--md-sys-color-secondary);
}

View File

@ -80,6 +80,31 @@ document.addEventListener('keydown', function(e) {
}); });
// Initialize page // Initialize page
// Theme change listener to redraw charts when theme changes
function setupThemeChangeListener() {
// Watch for theme changes (usually by a class on body or html element)
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'data-bs-theme' || mutation.attributeName === 'class') {
// Redraw charts with new theme colors if they exist
if (typeChart && userChart && timeChart) {
debugLog('Theme changed, redrawing charts');
// If we have stats data cached, use it
if (window.cachedStatsData) {
renderCharts(window.cachedStatsData);
}
}
}
});
});
// Observe the document element for theme changes
observer.observe(document.documentElement, { attributes: true });
// Also observe body for class changes
observer.observe(document.body, { attributes: true });
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
debugLog('Page initialized'); debugLog('Page initialized');
@ -106,7 +131,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Show a loading message immediately // Show a loading message immediately
if (auditTableBody) { if (auditTableBody) {
auditTableBody.innerHTML = auditTableBody.innerHTML =
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> Loading audit data...</td></tr>'; '<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> ' + window.i18n.loading + '</td></tr>';
} else { } else {
debugLog('ERROR: auditTableBody element not found!'); debugLog('ERROR: auditTableBody element not found!');
} }
@ -117,6 +142,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Load statistics for dashboard // Load statistics for dashboard
loadStats(7); loadStats(7);
// Setup theme change listener
setupThemeChangeListener();
// Set up event listeners // Set up event listeners
pageSizeSelect.addEventListener('change', function() { pageSizeSelect.addEventListener('change', function() {
pageSize = parseInt(this.value); pageSize = parseInt(this.value);
@ -350,7 +378,7 @@ function loadAuditData(targetPage, realPageSize) {
.catch(error => { .catch(error => {
debugLog('Error loading data', error.message); debugLog('Error loading data', error.message);
if (auditTableBody) { if (auditTableBody) {
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">Error loading data: ${error.message}</td></tr>`; auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">${window.i18n.errorLoading} ${error.message}</td></tr>`;
} }
hideLoading('table-loading'); hideLoading('table-loading');
@ -375,6 +403,8 @@ function loadStats(days) {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
document.getElementById('total-events').textContent = data.totalEvents; document.getElementById('total-events').textContent = data.totalEvents;
// Cache stats data for theme changes
window.cachedStatsData = data;
renderCharts(data); renderCharts(data);
hideLoading('type-chart-loading'); hideLoading('type-chart-loading');
hideLoading('user-chart-loading'); hideLoading('user-chart-loading');
@ -412,7 +442,7 @@ function renderTable(events) {
if (!events || events.length === 0) { if (!events || events.length === 0) {
debugLog('No events to render'); debugLog('No events to render');
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">No audit events found matching the current filters</td></tr>'; auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.noEventsFound + '</td></tr>';
return; return;
} }
@ -452,7 +482,7 @@ function renderTable(events) {
debugLog('Table rendering complete'); debugLog('Table rendering complete');
} catch (e) { } catch (e) {
debugLog('Error in renderTable', e.message); debugLog('Error in renderTable', e.message);
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error rendering table: ' + e.message + '</td></tr>'; auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.errorRendering + ' ' + e.message + '</td></tr>';
} }
} }
@ -521,6 +551,9 @@ function goToPage(page) {
// Render charts // Render charts
function renderCharts(data) { function renderCharts(data) {
// Get theme colors
const colors = getThemeColors();
// Prepare data for charts // Prepare data for charts
const typeLabels = Object.keys(data.eventsByType); const typeLabels = Object.keys(data.eventsByType);
const typeValues = Object.values(data.eventsByType); const typeValues = Object.values(data.eventsByType);
@ -532,6 +565,10 @@ function renderCharts(data) {
const timeLabels = Object.keys(data.eventsByDay).sort(); const timeLabels = Object.keys(data.eventsByDay).sort();
const timeValues = timeLabels.map(day => data.eventsByDay[day] || 0); const timeValues = timeLabels.map(day => data.eventsByDay[day] || 0);
// Chart.js global defaults for dark mode compatibility
Chart.defaults.color = colors.text;
Chart.defaults.borderColor = colors.grid;
// Type chart // Type chart
if (typeChart) { if (typeChart) {
typeChart.destroy(); typeChart.destroy();
@ -543,19 +580,84 @@ function renderCharts(data) {
data: { data: {
labels: typeLabels, labels: typeLabels,
datasets: [{ datasets: [{
label: 'Events by Type', label: window.i18n.eventsByType,
data: typeValues, data: typeValues,
backgroundColor: getChartColors(typeLabels.length), backgroundColor: colors.chartColors.slice(0, typeLabels.length),
borderColor: getChartColors(typeLabels.length, 1), // Full opacity for borders borderColor: colors.chartColors.slice(0, typeLabels.length),
borderWidth: 1 borderWidth: 1
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 14
}
}
},
tooltip: {
titleFont: {
weight: 'bold',
size: 14
},
bodyFont: {
size: 13
},
backgroundColor: colors.isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)',
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
borderColor: colors.grid,
borderWidth: 1
}
},
scales: { scales: {
y: { y: {
beginAtZero: true beginAtZero: true,
ticks: {
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 12
}
},
grid: {
color: colors.grid
},
title: {
display: true,
text: 'Count',
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 14
}
}
},
x: {
ticks: {
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 12
}
},
grid: {
color: colors.grid
},
title: {
display: true,
text: 'Event Type',
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 14
}
}
} }
} }
} }
@ -572,15 +674,58 @@ function renderCharts(data) {
data: { data: {
labels: userLabels, labels: userLabels,
datasets: [{ datasets: [{
label: 'Events by User', label: window.i18n.eventsByUser,
data: userValues, data: userValues,
backgroundColor: getChartColors(userLabels.length), backgroundColor: colors.chartColors.slice(0, userLabels.length),
borderWidth: 1 borderWidth: 1,
borderColor: colors.isDarkMode ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.2)'
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
color: colors.text,
font: {
size: colors.isDarkMode ? 14 : 12,
weight: colors.isDarkMode ? 'bold' : 'normal'
},
padding: 15,
// Add a box around each label for better contrast in dark mode
generateLabels: function(chart) {
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
const labels = original.call(this, chart);
if (colors.isDarkMode) {
labels.forEach(label => {
label.fillStyle = 'rgba(0, 0, 0, 0.7)'; // Dark background for text
label.strokeStyle = label.strokeStyle; // Keep original color for border
label.lineWidth = 2; // Thicker border
});
}
return labels;
}
}
},
tooltip: {
titleFont: {
weight: 'bold',
size: 14
},
bodyFont: {
size: 13
},
backgroundColor: colors.isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)',
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
borderColor: colors.grid,
borderWidth: 1
}
}
} }
}); });
@ -595,10 +740,10 @@ function renderCharts(data) {
data: { data: {
labels: timeLabels, labels: timeLabels,
datasets: [{ datasets: [{
label: 'Events Over Time', label: window.i18n.eventsOverTime,
data: timeValues, data: timeValues,
backgroundColor: 'rgba(75, 192, 192, 0.2)', backgroundColor: colors.chartColors[0] + '40', // 40 = 25% opacity
borderColor: 'rgba(75, 192, 192, 1)', borderColor: colors.chartColors[0],
tension: 0.1, tension: 0.1,
fill: true fill: true
}] }]
@ -606,9 +751,74 @@ function renderCharts(data) {
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 14
}
}
},
tooltip: {
titleFont: {
weight: 'bold',
size: 14
},
bodyFont: {
size: 13
},
backgroundColor: colors.isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)',
titleColor: colors.isDarkMode ? '#ffffff' : '#000000',
bodyColor: colors.isDarkMode ? '#ffffff' : '#000000',
borderColor: colors.grid,
borderWidth: 1
}
},
scales: { scales: {
y: { y: {
beginAtZero: true beginAtZero: true,
ticks: {
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 12
}
},
grid: {
color: colors.grid
},
title: {
display: true,
text: 'Number of Events',
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 14
}
}
},
x: {
ticks: {
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 12
}
},
grid: {
color: colors.grid
},
title: {
display: true,
text: 'Date',
color: colors.text,
font: {
weight: colors.isDarkMode ? 'bold' : 'normal',
size: 14
}
}
} }
} }
} }
@ -684,8 +894,63 @@ function loadEventTypes() {
}); });
} }
// Get theme colors for charts
function getThemeColors() {
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
// In dark mode, use higher contrast colors for text
const textColor = isDarkMode ?
'rgb(255, 255, 255)' : // White for dark mode for maximum contrast
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-on-surface').trim();
// Use a more visible grid color in dark mode
const gridColor = isDarkMode ?
'rgba(255, 255, 255, 0.2)' : // Semi-transparent white for dark mode
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-outline-variant').trim();
return {
text: textColor,
grid: gridColor,
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-surface-container').trim(),
chartColors: [
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-primary').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-secondary').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-tertiary').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-other').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-convert').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-sign').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-security').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--md-nav-section-color-convertto').trim(),
],
isDarkMode: isDarkMode
};
}
// Function to generate a palette of colors for charts // Function to generate a palette of colors for charts
function getChartColors(count, opacity = 0.6) { function getChartColors(count, opacity = 0.6) {
try {
// Use theme colors first
const themeColors = getThemeColors();
if (themeColors && themeColors.chartColors && themeColors.chartColors.length > 0) {
const result = [];
for (let i = 0; i < count; i++) {
// Get the raw color and add opacity
let color = themeColors.chartColors[i % themeColors.chartColors.length];
// If it's rgb() format, convert to rgba()
if (color.startsWith('rgb(')) {
color = color.replace('rgb(', '').replace(')', '');
result.push(`rgba(${color}, ${opacity})`);
} else {
// Just use the color directly
result.push(color);
}
}
return result;
}
} catch (e) {
console.warn('Error using theme colors, falling back to default colors', e);
}
// Base colors - a larger palette than the default // Base colors - a larger palette than the default
const colors = [ const colors = [
[54, 162, 235], // blue [54, 162, 235], // blue

View File

@ -0,0 +1,42 @@
# Audit System Help
## About the Audit System
The Stirling PDF audit system records user actions and system events for security monitoring, compliance, and troubleshooting purposes.
## Audit Levels
| Level | Name | Description | Use Case |
|-------|------|-------------|----------|
| 0 | OFF | Minimal auditing, only critical security events | Development environments |
| 1 | BASIC | Authentication events, security events, and errors | Production environments with minimal storage |
| 2 | STANDARD | All HTTP requests and operations (default) | Normal production use |
| 3 | VERBOSE | Detailed information including headers, parameters, and results | Troubleshooting and detailed analysis |
## Configuration
Audit settings are configured in the `settings.yml` file under the `premium.proFeatures.audit` section:
```yaml
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
```
## Common Event Types
### BASIC Events:
- USER_LOGIN - User login
- USER_LOGOUT - User logout
- USER_FAILED_LOGIN - Failed login attempt
- USER_PROFILE_UPDATE - User or profile operations
### STANDARD Events:
- HTTP_REQUEST - GET requests for viewing
- PDF_PROCESS - PDF processing operations
- FILE_OPERATION - File-related operations
- SETTINGS_CHANGED - System or admin settings operations
### VERBOSE Events:
- Detailed versions of STANDARD events with parameters and results

File diff suppressed because it is too large Load Diff

View File

@ -1636,6 +1636,83 @@ validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
# Audit Dashboard
audit.dashboard.title=Audit Dashboard
audit.dashboard.systemStatus=Audit System Status
audit.dashboard.status=Status
audit.dashboard.enabled=Enabled
audit.dashboard.disabled=Disabled
audit.dashboard.currentLevel=Current Level
audit.dashboard.retentionPeriod=Retention Period
audit.dashboard.days=days
audit.dashboard.totalEvents=Total Events
# Audit Dashboard Tabs
audit.dashboard.tab.dashboard=Dashboard
audit.dashboard.tab.events=Audit Events
audit.dashboard.tab.export=Export
# Dashboard Charts
audit.dashboard.eventsByType=Events by Type
audit.dashboard.eventsByUser=Events by User
audit.dashboard.eventsOverTime=Events Over Time
audit.dashboard.period.7days=7 Days
audit.dashboard.period.30days=30 Days
audit.dashboard.period.90days=90 Days
# Events Tab
audit.dashboard.auditEvents=Audit Events
audit.dashboard.filter.eventType=Event Type
audit.dashboard.filter.allEventTypes=All event types
audit.dashboard.filter.user=User
audit.dashboard.filter.userPlaceholder=Filter by user
audit.dashboard.filter.startDate=Start Date
audit.dashboard.filter.endDate=End Date
audit.dashboard.filter.apply=Apply Filters
audit.dashboard.filter.reset=Reset Filters
# Table Headers
audit.dashboard.table.id=ID
audit.dashboard.table.time=Time
audit.dashboard.table.user=User
audit.dashboard.table.type=Type
audit.dashboard.table.details=Details
# Pagination
audit.dashboard.pagination.show=Show
audit.dashboard.pagination.entries=entries
audit.dashboard.pagination.pageInfo1=Page
audit.dashboard.pagination.pageInfo2=of
audit.dashboard.pagination.totalRecords=Total records:
# Modal
audit.dashboard.modal.eventDetails=Event Details
audit.dashboard.modal.id=ID
audit.dashboard.modal.user=User
audit.dashboard.modal.type=Type
audit.dashboard.modal.time=Time
audit.dashboard.modal.data=Data
# Export Tab
audit.dashboard.export.title=Export Audit Data
audit.dashboard.export.format=Export Format
audit.dashboard.export.csv=CSV (Comma Separated Values)
audit.dashboard.export.json=JSON (JavaScript Object Notation)
audit.dashboard.export.button=Export Data
audit.dashboard.export.infoTitle=Export Information
audit.dashboard.export.infoDesc1=The export will include all audit events matching the selected filters. For large datasets, the export may take a few moments to generate.
audit.dashboard.export.infoDesc2=Exported data will include:
audit.dashboard.export.infoItem1=Event ID
audit.dashboard.export.infoItem2=User
audit.dashboard.export.infoItem3=Event Type
audit.dashboard.export.infoItem4=Timestamp
audit.dashboard.export.infoItem5=Event Data
# JavaScript i18n keys
audit.dashboard.js.noEventsFound=No audit events found matching the current filters
audit.dashboard.js.errorLoading=Error loading data:
audit.dashboard.js.errorRendering=Error rendering table:
audit.dashboard.js.loadingPage=Loading page
#################### ####################
# Cookie banner # # Cookie banner #
#################### ####################