Files
roa2web-service-auto/src/modules/reports/views/ServerLogsView.vue
Claude Agent 02a8c8682c feat: Add Linux deployment scripts and server logs view
- Add deployment/linux/ with deploy.sh for deploying from Claude-Agent LXC to Windows server
- Add ServerLogsView.vue for viewing server logs from frontend
- Add shared/routes/system.py for system health endpoints
- Update CLAUDE.md with quick deploy instructions
- Improve Windows deployment scripts (ROA2WEB-Console.ps1)
- Fix OCR service validation and worker pool improvements
- Update environment config examples
- Various script permission and startup fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 00:26:36 +00:00

390 lines
8.6 KiB
Vue

<template>
<div class="server-logs-view">
<div class="stats-header">
<h1>
<i class="pi pi-file-edit"></i>
Server Logs
</h1>
<div class="actions">
<Dropdown
v-model="selectedFile"
:options="logFiles"
optionLabel="label"
optionValue="value"
placeholder="Select log file"
class="log-file-select"
/>
<InputText
v-model="filterText"
placeholder="Filter (e.g., ocr, error)"
class="filter-input"
@keyup.enter="loadLogs"
/>
<Dropdown
v-model="linesCount"
:options="linesOptions"
optionLabel="label"
optionValue="value"
class="lines-select"
/>
<Button
label="Refresh"
icon="pi pi-refresh"
@click="loadLogs"
:loading="loading"
/>
<Button
label="Auto"
:icon="autoRefresh ? 'pi pi-pause' : 'pi pi-play'"
:severity="autoRefresh ? 'warning' : 'secondary'"
@click="toggleAutoRefresh"
/>
</div>
</div>
<Message v-if="error" severity="error" :closable="true" @close="error = null">
{{ error }}
</Message>
<Card class="logs-card">
<template #title>
<div class="logs-title">
<span>{{ selectedFile === 'backend-stderr' ? 'Errors & Warnings' : 'Info Logs' }}</span>
<Tag :value="`${logs.length} lines`" severity="info" />
<Tag v-if="debugInfo.file_size_kb !== null" :value="`${debugInfo.file_size_kb} KB`" severity="secondary" class="ml-2" />
<span v-if="autoRefresh" class="auto-refresh-indicator">
<i class="pi pi-spin pi-sync"></i> Auto-refresh: {{ autoRefreshInterval }}s
</span>
</div>
<div v-if="debugInfo.logs_path" class="logs-path">
<small><i class="pi pi-folder"></i> {{ debugInfo.logs_path }}</small>
</div>
</template>
<template #content>
<div v-if="loading && logs.length === 0" class="loading-container">
<ProgressSpinner />
<p>Loading logs...</p>
</div>
<div v-else-if="logs.length === 0" class="empty-logs">
<i class="pi pi-inbox"></i>
<p>No log entries found</p>
</div>
<div v-else class="logs-container" ref="logsContainer">
<pre class="logs-content"><code v-for="(line, index) in logs" :key="index" :class="getLineClass(line)">{{ line }}</code></pre>
</div>
</template>
</Card>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import Button from 'primevue/button'
import Card from 'primevue/card'
import Dropdown from 'primevue/dropdown'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import ProgressSpinner from 'primevue/progressspinner'
import Tag from 'primevue/tag'
import axios from 'axios'
// System API - endpoint separat de reports
const systemApi = axios.create({
baseURL: import.meta.env.BASE_URL + 'api/system',
headers: { 'Content-Type': 'application/json' }
})
// Add auth token
systemApi.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// State
const logs = ref([])
const loading = ref(false)
const error = ref(null)
const selectedFile = ref('backend-stderr')
const filterText = ref('')
const linesCount = ref(100)
const autoRefresh = ref(false)
const autoRefreshInterval = ref(5)
const logsContainer = ref(null)
const debugInfo = ref({
logs_path: null,
file_exists: true,
file_size_kb: null
})
let refreshTimer = null
// Options
const logFiles = [
{ label: 'Errors (stderr)', value: 'backend-stderr' },
{ label: 'Info (stdout)', value: 'backend-stdout' }
]
const linesOptions = [
{ label: '50 lines', value: 50 },
{ label: '100 lines', value: 100 },
{ label: '200 lines', value: 200 },
{ label: '500 lines', value: 500 }
]
// Methods
const loadLogs = async () => {
loading.value = true
error.value = null
try {
const params = {
file: selectedFile.value,
lines: linesCount.value
}
if (filterText.value) {
params.filter = filterText.value
}
console.log('[ServerLogs] Fetching logs from:', systemApi.defaults.baseURL + '/logs', params)
const response = await systemApi.get('/logs', { params })
console.log('[ServerLogs] Response:', response.data)
logs.value = response.data.lines || []
debugInfo.value = {
logs_path: response.data.logs_path || null,
file_exists: response.data.file_exists ?? true,
file_size_kb: response.data.file_size_kb ?? null
}
// Scroll to bottom
await nextTick()
if (logsContainer.value) {
logsContainer.value.scrollTop = logsContainer.value.scrollHeight
}
} catch (err) {
console.error('[ServerLogs] Failed to load logs:', err)
console.error('[ServerLogs] Error details:', err.response?.data, err.message)
error.value = err.response?.data?.detail || err.message || 'Failed to load logs - check console for details'
} finally {
loading.value = false
}
}
const getLineClass = (line) => {
const lineLower = line.toLowerCase()
if (lineLower.includes('error') || lineLower.includes('exception') || lineLower.includes('failed')) {
return 'log-error'
}
if (lineLower.includes('warning') || lineLower.includes('warn')) {
return 'log-warning'
}
if (lineLower.includes('success') || lineLower.includes('completed')) {
return 'log-success'
}
return 'log-info'
}
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
if (autoRefresh.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
const startAutoRefresh = () => {
stopAutoRefresh()
refreshTimer = setInterval(() => {
loadLogs()
}, autoRefreshInterval.value * 1000)
}
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
// Watchers
watch(selectedFile, () => {
loadLogs()
})
watch(linesCount, () => {
loadLogs()
})
// Lifecycle
onMounted(() => {
loadLogs()
})
onUnmounted(() => {
stopAutoRefresh()
})
</script>
<style scoped>
.server-logs-view {
padding: var(--space-lg);
max-width: 1400px;
margin: 0 auto;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-lg);
flex-wrap: wrap;
gap: var(--space-md);
}
.stats-header h1 {
margin: 0;
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: 1.5rem;
color: var(--color-text);
}
.actions {
display: flex;
gap: var(--space-sm);
flex-wrap: wrap;
align-items: center;
}
.log-file-select {
min-width: 150px;
}
.filter-input {
min-width: 180px;
}
.lines-select {
min-width: 120px;
}
.logs-card {
margin-top: var(--space-md);
}
.logs-title {
display: flex;
align-items: center;
gap: var(--space-md);
}
.logs-path {
margin-top: var(--space-xs);
color: var(--color-text-secondary, #6b7280);
font-size: 0.75rem;
}
.logs-path i {
margin-right: var(--space-xs);
}
.ml-2 {
margin-left: 0.5rem;
}
.auto-refresh-indicator {
font-size: 0.875rem;
color: var(--color-warning);
display: flex;
align-items: center;
gap: var(--space-xs);
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-xl);
color: var(--color-text-secondary);
}
.empty-logs {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-xl);
color: var(--color-text-secondary);
}
.empty-logs i {
font-size: 3rem;
margin-bottom: var(--space-md);
}
.logs-container {
max-height: 600px;
overflow-y: auto;
background: var(--color-bg-secondary, #1e1e1e);
border-radius: var(--radius-md);
padding: var(--space-sm);
}
.logs-content {
margin: 0;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.8125rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
.logs-content code {
display: block;
padding: 2px var(--space-xs);
border-radius: 2px;
}
.log-error {
color: #f87171;
background: rgba(248, 113, 113, 0.1);
}
.log-warning {
color: #fbbf24;
background: rgba(251, 191, 36, 0.1);
}
.log-success {
color: #34d399;
background: rgba(52, 211, 153, 0.1);
}
.log-info {
color: #e5e7eb;
}
/* Responsive */
@media (max-width: 768px) {
.stats-header {
flex-direction: column;
align-items: flex-start;
}
.actions {
width: 100%;
}
.log-file-select,
.filter-input,
.lines-select {
flex: 1;
min-width: 100px;
}
}
</style>