Add API logging functionality and enhance scheduler service
Introduced ApiLog model and integrated logging for scheduled tasks in the SchedulerService. Updated server.js to include request logging middleware and new API log routes. Enhanced frontend navigation by adding a link to system logs for admin users. Adjusted session check interval in App.vue for improved performance. This update improves monitoring and debugging capabilities across the application.
This commit is contained in:
@@ -22,6 +22,10 @@
|
||||
<span class="dropdown-icon">🔐</span>
|
||||
Berechtigungen
|
||||
</router-link>
|
||||
<router-link v-if="isAdmin" to="/logs" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">📋</span>
|
||||
System-Logs
|
||||
</router-link>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button @click="logout" class="dropdown-item logout-item">
|
||||
<span class="dropdown-icon">🚪</span>
|
||||
@@ -196,6 +200,11 @@ export default {
|
||||
// Owner oder Admin können Freigaben verwalten
|
||||
return this.isClubOwner || this.userRole === 'admin' || this.hasPermission('approvals', 'read');
|
||||
},
|
||||
isAdmin() {
|
||||
// Nur anzeigen, wenn Permissions geladen sind UND Admin-Rechte vorhanden
|
||||
if (!this.currentClub) return false;
|
||||
return this.isClubOwner || this.userRole === 'admin';
|
||||
},
|
||||
canManagePermissions() {
|
||||
// Nur anzeigen, wenn Permissions geladen sind UND Berechtigung vorhanden
|
||||
if (!this.currentClub) return false;
|
||||
@@ -282,7 +291,8 @@ export default {
|
||||
this.selectedClub = this.currentClub;
|
||||
}
|
||||
this.checkSession();
|
||||
this.sessionInterval = setInterval(this.checkSession, 5000);
|
||||
// Session-Check alle 30 Sekunden
|
||||
this.sessionInterval = setInterval(this.checkSession, 30000);
|
||||
} catch (error) {
|
||||
this.setClubs([]);
|
||||
this.selectedClub = null;
|
||||
@@ -327,7 +337,8 @@ export default {
|
||||
this.selectedClub = this.currentClub;
|
||||
}
|
||||
this.checkSession();
|
||||
this.sessionInterval = setInterval(this.checkSession, 5000);
|
||||
// Session-Check alle 30 Sekunden
|
||||
this.sessionInterval = setInterval(this.checkSession, 30000);
|
||||
} catch (error) {
|
||||
this.setClubs([]);
|
||||
this.selectedClub = null;
|
||||
|
||||
@@ -16,6 +16,7 @@ import OfficialTournaments from './views/OfficialTournaments.vue';
|
||||
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
|
||||
import TeamManagementView from './views/TeamManagementView.vue';
|
||||
import PermissionsView from './views/PermissionsView.vue';
|
||||
import LogsView from './views/LogsView.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
import Datenschutz from './views/Datenschutz.vue';
|
||||
|
||||
@@ -37,6 +38,7 @@ const routes = [
|
||||
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
|
||||
{ path: '/team-management', component: TeamManagementView },
|
||||
{ path: '/permissions', component: PermissionsView },
|
||||
{ path: '/logs', component: LogsView },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
{ path: '/datenschutz', component: Datenschutz },
|
||||
];
|
||||
|
||||
754
frontend/src/views/LogsView.vue
Normal file
754
frontend/src/views/LogsView.vue
Normal file
@@ -0,0 +1,754 @@
|
||||
<template>
|
||||
<div class="logs-view">
|
||||
<div class="header">
|
||||
<h1>System-Logs</h1>
|
||||
<p class="subtitle">Übersicht über alle API-Requests, Responses und Ausführungen</p>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<div class="filter-controls">
|
||||
<div class="filter-group">
|
||||
<label>Log-Typ:</label>
|
||||
<select v-model="filters.logType" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="api_request">API-Requests</option>
|
||||
<option value="scheduler">Scheduler</option>
|
||||
<option value="cron_job">Cron-Jobs</option>
|
||||
<option value="manual">Manuelle Ausführungen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>HTTP-Methode:</label>
|
||||
<select v-model="filters.method" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Status:</label>
|
||||
<select v-model="filters.statusCode" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="200">200 - OK</option>
|
||||
<option value="400">400 - Bad Request</option>
|
||||
<option value="401">401 - Unauthorized</option>
|
||||
<option value="403">403 - Forbidden</option>
|
||||
<option value="404">404 - Not Found</option>
|
||||
<option value="500">500 - Server Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Pfad:</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="filters.path"
|
||||
placeholder="z.B. /api/diary"
|
||||
class="filter-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Von:</label>
|
||||
<input type="date" v-model="filters.startDate" class="filter-input" />
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Bis:</label>
|
||||
<input type="date" v-model="filters.endDate" class="filter-input" />
|
||||
</div>
|
||||
|
||||
<button @click="applyFilters" class="btn-primary">Filter anwenden</button>
|
||||
<button @click="clearFilters" class="btn-secondary">Zurücksetzen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status-Banner -->
|
||||
<div v-if="lastLoadTime" class="status-banner" :class="statusBannerClass">
|
||||
<div class="status-content">
|
||||
<span class="status-icon">{{ statusIcon }}</span>
|
||||
<span class="status-text">
|
||||
<template v-if="!loading && !error">
|
||||
<strong>Erfolgreich geladen</strong> – {{ total }} Datensätze gefunden, {{ logs.length }} angezeigt
|
||||
</template>
|
||||
<template v-else-if="loading">
|
||||
Lade Logs...
|
||||
</template>
|
||||
<template v-else-if="error">
|
||||
<strong>Fehler:</strong> {{ error }}
|
||||
</template>
|
||||
</span>
|
||||
<span class="status-time">{{ formatLastLoadTime(lastLoadTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !lastLoadTime" class="loading">Lade Logs...</div>
|
||||
<div v-else-if="error && !lastLoadTime" class="error">{{ error }}</div>
|
||||
<div v-else class="logs-content">
|
||||
<div class="logs-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Gesamt:</span>
|
||||
<span class="stat-value">{{ total }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Angezeigt:</span>
|
||||
<span class="stat-value">{{ logs.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-table-container">
|
||||
<table class="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeit</th>
|
||||
<th>Typ</th>
|
||||
<th>Methode</th>
|
||||
<th>Pfad</th>
|
||||
<th>Status</th>
|
||||
<th>Ausführungszeit</th>
|
||||
<th>Fehler</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs" :key="log.id" :class="getLogRowClass(log)">
|
||||
<td>{{ formatDate(log.createdAt) }}</td>
|
||||
<td>
|
||||
<span class="log-type-badge" :class="`log-type-${log.logType}`">
|
||||
{{ getLogTypeLabel(log.logType) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="log.method !== 'SCHEDULER'" class="method-badge" :class="`method-${log.method}`">
|
||||
{{ log.method }}
|
||||
</span>
|
||||
<span v-else class="scheduler-badge">⏰</span>
|
||||
</td>
|
||||
<td class="path-cell">{{ log.path }}</td>
|
||||
<td>
|
||||
<span class="status-badge" :class="getStatusClass(log.statusCode)">
|
||||
{{ log.statusCode || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatExecutionTime(log.executionTime) }}</td>
|
||||
<td>
|
||||
<span v-if="log.errorMessage" class="error-indicator" :title="log.errorMessage">
|
||||
⚠️ {{ truncate(log.errorMessage, 50) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>
|
||||
<button @click="viewLogDetails(log)" class="btn-view">Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button
|
||||
@click="previousPage"
|
||||
:disabled="offset === 0"
|
||||
class="btn-pagination"
|
||||
>
|
||||
← Vorherige
|
||||
</button>
|
||||
<span class="page-info">
|
||||
Seite {{ currentPage }} von {{ totalPages }}
|
||||
</span>
|
||||
<button
|
||||
@click="nextPage"
|
||||
:disabled="offset + logs.length >= total"
|
||||
class="btn-pagination"
|
||||
>
|
||||
Nächste →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Details Dialog -->
|
||||
<InfoDialog
|
||||
v-model="logDetailsDialog.isOpen"
|
||||
:title="logDetailsDialog.title"
|
||||
:message="logDetailsDialog.message"
|
||||
:details="logDetailsDialog.details"
|
||||
:type="logDetailsDialog.type"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import apiClient from '../apiClient.js';
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'LogsView',
|
||||
components: {
|
||||
InfoDialog
|
||||
},
|
||||
setup() {
|
||||
const logs = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const limit = ref(50);
|
||||
const offset = ref(0);
|
||||
const lastLoadTime = ref(null);
|
||||
|
||||
const filters = ref({
|
||||
logType: '',
|
||||
method: '',
|
||||
statusCode: '',
|
||||
path: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
});
|
||||
|
||||
const logDetailsDialog = ref({
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info'
|
||||
});
|
||||
|
||||
const currentPage = computed(() => Math.floor(offset.value / limit.value) + 1);
|
||||
const totalPages = computed(() => Math.ceil(total.value / limit.value));
|
||||
|
||||
const loadLogs = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
limit: limit.value,
|
||||
offset: offset.value,
|
||||
...filters.value
|
||||
};
|
||||
|
||||
// Remove empty filters
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null) {
|
||||
delete params[key];
|
||||
}
|
||||
});
|
||||
|
||||
const response = await apiClient.get('/logs', { params });
|
||||
|
||||
if (response.data.success) {
|
||||
logs.value = response.data.data.logs;
|
||||
total.value = response.data.data.total;
|
||||
error.value = null;
|
||||
lastLoadTime.value = new Date();
|
||||
} else {
|
||||
error.value = 'Fehler beim Laden der Logs';
|
||||
lastLoadTime.value = new Date();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading logs:', err);
|
||||
error.value = err.response?.data?.error || 'Fehler beim Laden der Logs';
|
||||
lastLoadTime.value = new Date();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
offset.value = 0;
|
||||
loadLogs();
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
filters.value = {
|
||||
logType: '',
|
||||
method: '',
|
||||
statusCode: '',
|
||||
path: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
};
|
||||
offset.value = 0;
|
||||
loadLogs();
|
||||
};
|
||||
|
||||
const previousPage = () => {
|
||||
if (offset.value >= limit.value) {
|
||||
offset.value -= limit.value;
|
||||
loadLogs();
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (offset.value + logs.value.length < total.value) {
|
||||
offset.value += limit.value;
|
||||
loadLogs();
|
||||
}
|
||||
};
|
||||
|
||||
const viewLogDetails = async (log) => {
|
||||
try {
|
||||
const response = await apiClient.get(`/logs/${log.id}`);
|
||||
|
||||
if (response.data.success) {
|
||||
const logDetails = response.data.data;
|
||||
logDetailsDialog.value = {
|
||||
isOpen: true,
|
||||
title: `Log-Details #${log.id}`,
|
||||
message: `${logDetails.method} ${logDetails.path}`,
|
||||
details: formatLogDetails(logDetails),
|
||||
type: logDetails.statusCode >= 400 ? 'error' : 'info'
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading log details:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const formatLogDetails = (log) => {
|
||||
let details = `Zeit: ${formatDate(log.createdAt)}\n`;
|
||||
details += `Typ: ${getLogTypeLabel(log.logType)}\n`;
|
||||
details += `Methode: ${log.method}\n`;
|
||||
details += `Pfad: ${log.path}\n`;
|
||||
|
||||
if (log.statusCode) {
|
||||
details += `Status: ${log.statusCode}\n`;
|
||||
}
|
||||
|
||||
if (log.executionTime) {
|
||||
details += `Ausführungszeit: ${formatExecutionTime(log.executionTime)}\n`;
|
||||
}
|
||||
|
||||
if (log.ipAddress) {
|
||||
details += `IP: ${log.ipAddress}\n`;
|
||||
}
|
||||
|
||||
if (log.schedulerJobType) {
|
||||
details += `Scheduler-Job: ${log.schedulerJobType}\n`;
|
||||
}
|
||||
|
||||
if (log.errorMessage) {
|
||||
details += `\nFehler:\n${log.errorMessage}\n`;
|
||||
}
|
||||
|
||||
if (log.requestBody) {
|
||||
details += `\nRequest Body:\n${log.requestBody}\n`;
|
||||
}
|
||||
|
||||
if (log.responseBody) {
|
||||
details += `\nResponse Body:\n${log.responseBody}\n`;
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const formatExecutionTime = (time) => {
|
||||
if (!time) return '-';
|
||||
if (time < 1000) return `${time}ms`;
|
||||
return `${(time / 1000).toFixed(2)}s`;
|
||||
};
|
||||
|
||||
const getLogTypeLabel = (type) => {
|
||||
const labels = {
|
||||
api_request: 'API-Request',
|
||||
scheduler: 'Scheduler',
|
||||
cron_job: 'Cron-Job',
|
||||
manual: 'Manuell'
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
const getLogRowClass = (log) => {
|
||||
return {
|
||||
'log-error': log.statusCode >= 400,
|
||||
'log-success': log.statusCode >= 200 && log.statusCode < 300,
|
||||
'log-scheduler': log.logType === 'scheduler'
|
||||
};
|
||||
};
|
||||
|
||||
const getStatusClass = (statusCode) => {
|
||||
if (!statusCode) return 'status-unknown';
|
||||
if (statusCode >= 200 && statusCode < 300) return 'status-success';
|
||||
if (statusCode >= 400 && statusCode < 500) return 'status-client-error';
|
||||
if (statusCode >= 500) return 'status-server-error';
|
||||
return 'status-info';
|
||||
};
|
||||
|
||||
const truncate = (str, maxLen) => {
|
||||
if (!str) return '';
|
||||
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
||||
};
|
||||
|
||||
const statusBannerClass = computed(() => {
|
||||
if (loading.value) return 'status-loading';
|
||||
if (error.value) return 'status-error';
|
||||
return 'status-success';
|
||||
});
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
if (loading.value) return '⏳';
|
||||
if (error.value) return '❌';
|
||||
return '✅';
|
||||
});
|
||||
|
||||
const formatLastLoadTime = (date) => {
|
||||
if (!date) return '';
|
||||
const now = new Date();
|
||||
const diff = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diff < 5) return 'gerade eben';
|
||||
if (diff < 60) return `vor ${diff} Sekunden`;
|
||||
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Minuten`;
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs();
|
||||
});
|
||||
|
||||
return {
|
||||
logs,
|
||||
total,
|
||||
loading,
|
||||
error,
|
||||
filters,
|
||||
logDetailsDialog,
|
||||
currentPage,
|
||||
totalPages,
|
||||
applyFilters,
|
||||
clearFilters,
|
||||
previousPage,
|
||||
nextPage,
|
||||
viewLogDetails,
|
||||
formatDate,
|
||||
formatExecutionTime,
|
||||
getLogTypeLabel,
|
||||
getLogRowClass,
|
||||
getStatusClass,
|
||||
truncate,
|
||||
lastLoadTime,
|
||||
statusBannerClass,
|
||||
statusIcon,
|
||||
formatLastLoadTime
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-view {
|
||||
padding: 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary, #666);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #dcfce7;
|
||||
border-color: #16a34a;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #fee2e2;
|
||||
border-color: #dc2626;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-loading {
|
||||
background: #fef3c7;
|
||||
border-color: #f59e0b;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.status-time {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
background: var(--background-light, #f8f9fa);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
.filter-select,
|
||||
.filter-input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.logs-stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color, #007bff);
|
||||
}
|
||||
|
||||
.logs-table-container {
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.logs-table thead {
|
||||
background: var(--background-light, #f8f9fa);
|
||||
}
|
||||
|
||||
.logs-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.logs-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.logs-table tbody tr:hover {
|
||||
background: var(--background-light, #f8f9fa);
|
||||
}
|
||||
|
||||
.log-error {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.log-success {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.log-scheduler {
|
||||
background: #fefce8;
|
||||
}
|
||||
|
||||
.log-type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-type-api_request {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.log-type-scheduler {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.log-type-cron_job {
|
||||
background: #fce7f3;
|
||||
color: #9f1239;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.method-GET {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.method-POST {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.method-PUT {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.method-DELETE {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-client-error {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-server-error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.path-cell {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error-indicator {
|
||||
color: #dc2626;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--primary-color, #007bff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.btn-view:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-pagination {
|
||||
padding: 0.5rem 1rem;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-pagination:hover:not(:disabled) {
|
||||
background: var(--background-light, #f8f9fa);
|
||||
}
|
||||
|
||||
.btn-pagination:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user