feat: nettoyage menu + suppression modules inexistants + log viewer
- Sidebar : retrait des liens files, logs, services (non implémentés) - Migration 001 : suppression des inserts files/logs/services - Migration 002 : DELETE des modules inexistants en DB existante - logbuffer : ring buffer mémoire branché sur log.SetOutput - GET /api/settings/logs : retourne les 300 dernières lignes de log - Settings : onglet Logs avec auto-refresh (5s/10s/30s/60s/désactivé, défaut 10s) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
07af66ad81
commit
88831e3967
9 changed files with 200 additions and 27 deletions
|
|
@ -101,24 +101,6 @@ const navItems = computed(() => {
|
|||
label: 'nav.terminal',
|
||||
icon: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
|
||||
},
|
||||
{
|
||||
name: 'files',
|
||||
path: '/files',
|
||||
label: 'nav.files',
|
||||
icon: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
|
||||
},
|
||||
{
|
||||
name: 'logs',
|
||||
path: '/logs',
|
||||
label: 'nav.logs',
|
||||
icon: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
|
||||
},
|
||||
{
|
||||
name: 'services',
|
||||
path: '/services',
|
||||
label: 'nav.services',
|
||||
icon: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93A10 10 0 1 0 4.93 19.07"/></svg>`,
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
path: '/settings',
|
||||
|
|
|
|||
|
|
@ -118,7 +118,11 @@
|
|||
"sidebarPosition": "Sidebar position",
|
||||
"left": "Left",
|
||||
"right": "Right",
|
||||
"noAuditLog": "No audit log entries"
|
||||
"noAuditLog": "No audit log entries",
|
||||
"logs": "Logs",
|
||||
"logsRefresh": "Auto-refresh",
|
||||
"logsNoRefresh": "Disabled",
|
||||
"noLogs": "No logs available"
|
||||
},
|
||||
"modules": {
|
||||
"desc": "Manage installed modules on ProxmoxPanel.",
|
||||
|
|
|
|||
|
|
@ -118,7 +118,11 @@
|
|||
"sidebarPosition": "Position de la sidebar",
|
||||
"left": "Gauche",
|
||||
"right": "Droite",
|
||||
"noAuditLog": "Aucune entrée dans le journal"
|
||||
"noAuditLog": "Aucune entrée dans le journal",
|
||||
"logs": "Logs",
|
||||
"logsRefresh": "Rafraîchissement",
|
||||
"logsNoRefresh": "Désactivé",
|
||||
"noLogs": "Aucun log disponible"
|
||||
},
|
||||
"modules": {
|
||||
"desc": "Gérez les modules installés sur ProxmoxPanel.",
|
||||
|
|
|
|||
|
|
@ -101,8 +101,32 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton sauvegarder (sauf audit) -->
|
||||
<div v-if="activeTab !== 'audit'" class="settings-actions">
|
||||
<!-- Logs applicatifs -->
|
||||
<div v-if="activeTab === 'logs'">
|
||||
<div class="logs-header">
|
||||
<h3>{{ t('settings.logs') }}</h3>
|
||||
<div class="logs-controls">
|
||||
<label class="text-muted">{{ t('settings.logsRefresh') }}</label>
|
||||
<select v-model="logsRefreshInterval" class="neu-input neu-input--sm" @change="resetLogsRefresh">
|
||||
<option :value="5000">5s</option>
|
||||
<option :value="10000">10s</option>
|
||||
<option :value="30000">30s</option>
|
||||
<option :value="60000">60s</option>
|
||||
<option :value="0">{{ t('settings.logsNoRefresh') }}</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="loadLogs">{{ t('common.refresh') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logs-viewer neu-inset" ref="logsViewerRef">
|
||||
<p v-if="logLines.length === 0" class="text-muted logs-empty">{{ t('settings.noLogs') }}</p>
|
||||
<div v-else class="logs-lines">
|
||||
<span v-for="(line, i) in logLines" :key="i" class="log-line">{{ line }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton sauvegarder (sauf audit et logs) -->
|
||||
<div v-if="activeTab !== 'audit' && activeTab !== 'logs'" class="settings-actions">
|
||||
<button class="neu-btn neu-btn--primary" :disabled="saving" @click="saveSettings">
|
||||
{{ saving ? t('common.saving') : t('common.save') }}
|
||||
</button>
|
||||
|
|
@ -114,7 +138,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { useUiStore } from '@/stores/ui.store'
|
||||
|
|
@ -127,12 +151,17 @@ const activeTab = ref('general')
|
|||
const saving = ref(false)
|
||||
const saveSuccess = ref(false)
|
||||
const auditLog = ref<any[]>([])
|
||||
const logLines = ref<string[]>([])
|
||||
const logsRefreshInterval = ref(10000)
|
||||
const logsViewerRef = ref<HTMLElement | null>(null)
|
||||
let logsTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: 'settings.general', icon: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/></svg>` },
|
||||
{ id: 'infrastructure', label: 'settings.infrastructure', icon: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/></svg>` },
|
||||
{ id: 'appearance', label: 'settings.appearance', icon: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>` },
|
||||
{ id: 'audit', label: 'settings.audit', icon: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12"/></svg>` },
|
||||
{ id: 'logs', label: 'settings.logs', icon: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>` },
|
||||
]
|
||||
|
||||
const settings = ref({
|
||||
|
|
@ -149,6 +178,18 @@ onMounted(async () => {
|
|||
await loadAuditLog()
|
||||
})
|
||||
|
||||
// Charger les logs quand on active l'onglet + gérer le timer
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'logs') {
|
||||
loadLogs()
|
||||
startLogsRefresh()
|
||||
} else {
|
||||
stopLogsRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => stopLogsRefresh())
|
||||
|
||||
async function loadSettings() {
|
||||
const res = await fetch('/api/settings', {
|
||||
headers: { Authorization: `Bearer ${authStore.accessToken}` },
|
||||
|
|
@ -195,6 +236,39 @@ function setSidebar(pos: 'left' | 'right') {
|
|||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleString()
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const res = await fetch('/api/settings/logs?lines=300', {
|
||||
headers: { Authorization: `Bearer ${authStore.accessToken}` },
|
||||
})
|
||||
if (res.ok) {
|
||||
logLines.value = await res.json() || []
|
||||
// Scroll vers le bas après rendu
|
||||
await nextTick()
|
||||
if (logsViewerRef.value) {
|
||||
logsViewerRef.value.scrollTop = logsViewerRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startLogsRefresh() {
|
||||
stopLogsRefresh()
|
||||
if (logsRefreshInterval.value > 0) {
|
||||
logsTimer = setInterval(loadLogs, logsRefreshInterval.value)
|
||||
}
|
||||
}
|
||||
|
||||
function stopLogsRefresh() {
|
||||
if (logsTimer) {
|
||||
clearInterval(logsTimer)
|
||||
logsTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function resetLogsRefresh() {
|
||||
stopLogsRefresh()
|
||||
startLogsRefresh()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -251,6 +325,38 @@ function formatDate(iso: string): string {
|
|||
|
||||
.text-muted { color: var(--neu-text-muted); }
|
||||
|
||||
.logs-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--neu-space-md); flex-wrap: wrap; gap: var(--neu-space-sm); }
|
||||
.logs-header h3 { margin-bottom: 0; }
|
||||
.logs-controls { display: flex; align-items: center; gap: var(--neu-space-sm); }
|
||||
.neu-input--sm { padding: 4px 8px; font-size: var(--neu-font-sm); height: auto; }
|
||||
|
||||
.logs-viewer {
|
||||
height: 420px;
|
||||
overflow-y: auto;
|
||||
padding: var(--neu-space-sm);
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logs-empty { padding: var(--neu-space-sm); }
|
||||
|
||||
.logs-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: var(--neu-text);
|
||||
padding: 1px 0;
|
||||
border-bottom: 1px solid var(--neu-border);
|
||||
}
|
||||
|
||||
.log-line:last-child { border-bottom: none; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.settings-layout { grid-template-columns: 1fr; }
|
||||
.settings-tabs { flex-direction: row; flex-wrap: wrap; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue