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:
Claude Agent
2026-01-04 00:26:36 +00:00
parent 495790411f
commit 02a8c8682c
39 changed files with 1939 additions and 80 deletions

155
shared/routes/system.py Normal file
View File

@@ -0,0 +1,155 @@
"""
System routes for server monitoring and logs.
"""
import os
from pathlib import Path
from typing import Optional
from collections import deque
from fastapi import APIRouter, Depends, Query, HTTPException
from pydantic import BaseModel
from shared.auth.dependencies import get_current_user, CurrentUser
class LogEntry(BaseModel):
"""Single log entry."""
line: str
level: Optional[str] = None
class LogsResponse(BaseModel):
"""Response with log entries."""
file: str
lines: list[str]
total_lines: int
showing: int
logs_path: Optional[str] = None
file_exists: bool = True
file_size_kb: Optional[float] = None
def create_system_router() -> APIRouter:
"""
Create system router for logs and monitoring.
"""
router = APIRouter()
def get_logs_path() -> Path:
"""Get logs directory path based on environment."""
# Windows production: C:\inetpub\wwwroot\roa2web\logs
# Development: backend/logs or ./logs
if os.name == 'nt': # Windows
prod_path = Path(r"C:\inetpub\wwwroot\roa2web\logs")
if prod_path.exists():
return prod_path
# Development fallback
dev_paths = [
Path(__file__).parent.parent.parent / "backend" / "logs",
Path(__file__).parent.parent.parent / "logs",
Path("./logs"),
]
for path in dev_paths:
if path.exists():
return path
return Path("./logs")
@router.get("/logs", response_model=LogsResponse)
async def get_logs(
file: str = Query(default="backend-stderr", description="Log file: backend-stderr or backend-stdout"),
lines: int = Query(default=100, ge=10, le=1000, description="Number of lines to return"),
filter: Optional[str] = Query(default=None, description="Filter text (case-insensitive)"),
current_user: CurrentUser = Depends(get_current_user)
):
"""
Get server log entries.
Args:
file: Log file name (backend-stderr or backend-stdout)
lines: Number of lines to return (10-1000)
filter: Optional filter text
Returns:
LogsResponse with log lines
"""
# Validate file name to prevent path traversal
allowed_files = ["backend-stderr", "backend-stdout"]
if file not in allowed_files:
raise HTTPException(status_code=400, detail=f"Invalid file. Allowed: {allowed_files}")
logs_path = get_logs_path()
log_file = logs_path / f"{file}.log"
logs_path_str = str(logs_path.resolve())
if not log_file.exists():
return LogsResponse(
file=file,
lines=[f"Log file not found: {log_file}"],
total_lines=0,
showing=0,
logs_path=logs_path_str,
file_exists=False,
file_size_kb=0
)
try:
# Get file size
file_size_kb = round(log_file.stat().st_size / 1024, 2)
# Read file and get last N lines efficiently
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
# Use deque for efficient tail operation
all_lines = deque(f, maxlen=lines * 2 if filter else lines)
# Apply filter if provided
if filter:
filter_lower = filter.lower()
filtered_lines = [line.rstrip() for line in all_lines if filter_lower in line.lower()]
result_lines = list(filtered_lines)[-lines:]
else:
result_lines = [line.rstrip() for line in all_lines][-lines:]
return LogsResponse(
file=file,
lines=result_lines,
total_lines=len(result_lines),
showing=len(result_lines),
logs_path=logs_path_str,
file_exists=True,
file_size_kb=file_size_kb
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading logs: {str(e)}")
@router.get("/logs/available")
async def get_available_logs(
current_user: CurrentUser = Depends(get_current_user)
):
"""
Get list of available log files.
"""
logs_path = get_logs_path()
if not logs_path.exists():
return {"logs_path": str(logs_path), "files": [], "exists": False}
log_files = []
for f in logs_path.glob("*.log"):
stat = f.stat()
log_files.append({
"name": f.stem,
"size_kb": round(stat.st_size / 1024, 1),
"modified": stat.st_mtime
})
return {
"logs_path": str(logs_path),
"files": sorted(log_files, key=lambda x: x["name"]),
"exists": True
}
return router