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>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
<ul class="menu-list">
|
||||
<li class="menu-item">
|
||||
<router-link
|
||||
to="/dashboard"
|
||||
to="/reports/dashboard"
|
||||
class="menu-link"
|
||||
:class="{ active: $route.name === 'Dashboard' }"
|
||||
@click="closeMenu"
|
||||
@@ -26,7 +26,7 @@
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<router-link
|
||||
to="/invoices"
|
||||
to="/reports/invoices"
|
||||
class="menu-link"
|
||||
:class="{ active: $route.name === 'Invoices' }"
|
||||
@click="closeMenu"
|
||||
@@ -37,9 +37,9 @@
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<router-link
|
||||
to="/bank-cash-register"
|
||||
to="/reports/bank-cash"
|
||||
class="menu-link"
|
||||
:class="{ active: $route.name === 'BankCashRegister' }"
|
||||
:class="{ active: $route.name === 'BankCash' }"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<i class="menu-icon pi pi-money-bill"></i>
|
||||
@@ -48,7 +48,7 @@
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<router-link
|
||||
to="/trial-balance"
|
||||
to="/reports/trial-balance"
|
||||
class="menu-link"
|
||||
:class="{ active: $route.name === 'TrialBalance' }"
|
||||
@click="closeMenu"
|
||||
@@ -66,7 +66,7 @@
|
||||
<ul class="menu-list">
|
||||
<li class="menu-item">
|
||||
<router-link
|
||||
to="/cache-stats"
|
||||
to="/reports/cache-stats"
|
||||
class="menu-link"
|
||||
:class="{ active: $route.name === 'CacheStats' }"
|
||||
@click="closeMenu"
|
||||
@@ -75,6 +75,17 @@
|
||||
<span>Statistici cache</span>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<router-link
|
||||
to="/reports/server-logs"
|
||||
class="menu-link"
|
||||
:class="{ active: $route.name === 'ServerLogs' }"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<i class="menu-icon pi pi-file-edit"></i>
|
||||
<span>Server Logs</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +98,7 @@
|
||||
<ul class="menu-list">
|
||||
<li class="menu-item">
|
||||
<router-link
|
||||
to="/telegram"
|
||||
to="/reports/telegram"
|
||||
class="menu-link"
|
||||
:class="{ active: $route.name === 'Telegram' }"
|
||||
@click="closeMenu"
|
||||
@@ -111,7 +122,7 @@
|
||||
<script>
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../../stores/auth";
|
||||
import { useAuthStore } from "@reports/stores/sharedStores";
|
||||
|
||||
export default {
|
||||
name: "HamburgerMenu",
|
||||
@@ -123,6 +134,8 @@ export default {
|
||||
},
|
||||
emits: ["close"],
|
||||
setup(props, { emit }) {
|
||||
console.log('[HamburgerMenu] Component loaded - Server Logs should be visible in Sistem section');
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
|
||||
389
src/modules/reports/views/ServerLogsView.vue
Normal file
389
src/modules/reports/views/ServerLogsView.vue
Normal file
@@ -0,0 +1,389 @@
|
||||
<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>
|
||||
@@ -79,7 +79,22 @@ import { useToast } from "primevue/usetoast";
|
||||
import Button from "primevue/button";
|
||||
import Toast from "primevue/toast";
|
||||
import QRCodeVue from "qrcode.vue";
|
||||
import api from "@reports/services/api";
|
||||
import axios from "axios";
|
||||
|
||||
// Telegram API uses /api/telegram (separate from reports)
|
||||
const telegramApi = axios.create({
|
||||
baseURL: import.meta.env.BASE_URL + 'api/telegram',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
// Add auth token
|
||||
telegramApi.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@@ -107,7 +122,7 @@ const generateCode = async () => {
|
||||
showQR.value = false;
|
||||
|
||||
try {
|
||||
const response = await api.post("/telegram/auth/generate-code");
|
||||
const response = await telegramApi.post("/auth/generate-code");
|
||||
linkingCode.value = response.data.linking_code;
|
||||
timeRemaining.value = response.data.expires_in_minutes * 60;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user