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:
@@ -18,7 +18,7 @@ from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends, Query
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends, Query, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.modules.data_entry.db.database import get_session
|
||||
@@ -190,7 +190,10 @@ async def get_job_status(
|
||||
@router.get("/jobs/{job_id}/wait", response_model=OCRJobResponse)
|
||||
async def wait_for_job_status(
|
||||
job_id: str,
|
||||
response: Response,
|
||||
timeout: int = Query(default=30, ge=1, le=60, description="Max wait time in seconds"),
|
||||
wait_for_terminal: bool = Query(default=False, description="If true, only return on completed/failed"),
|
||||
_t: int = Query(default=None, description="Cache-busting timestamp (ignored)"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
@@ -198,7 +201,8 @@ async def wait_for_job_status(
|
||||
Long-poll for OCR job status change.
|
||||
|
||||
Waits until:
|
||||
- Job status changes to completed/failed
|
||||
- Job status changes (default behavior - returns on any status change)
|
||||
- Job reaches terminal state (if wait_for_terminal=true)
|
||||
- Timeout expires (returns current status)
|
||||
|
||||
Recommended client timeout: timeout + 5 seconds
|
||||
@@ -206,36 +210,53 @@ async def wait_for_job_status(
|
||||
Args:
|
||||
job_id: Job UUID from POST /extract response
|
||||
timeout: Max wait time in seconds (1-60, default 30)
|
||||
wait_for_terminal: If true, wait until completed/failed only
|
||||
|
||||
Returns:
|
||||
OCRJobResponse with status, queue_position, and result (if completed)
|
||||
"""
|
||||
# Prevent caching - critical for long-polling
|
||||
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
response.headers["Expires"] = "0"
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
end_time = time.time() + timeout
|
||||
start_time = time.time()
|
||||
end_time = start_time + timeout
|
||||
last_status = None
|
||||
iteration = 0
|
||||
|
||||
print(f"[OCR Wait] Starting long-poll for job {job_id}, timeout={timeout}s, wait_for_terminal={wait_for_terminal}", flush=True)
|
||||
|
||||
while time.time() < end_time:
|
||||
iteration += 1
|
||||
job = await job_queue.get_job(job_id)
|
||||
|
||||
if not job:
|
||||
print(f"[OCR Wait] Job {job_id} not found after {iteration} iterations", flush=True)
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
# Return immediately if job completed or failed
|
||||
# Return immediately if job completed or failed (terminal states)
|
||||
if job.status in [JobStatus.completed, JobStatus.failed]:
|
||||
elapsed = time.time() - start_time
|
||||
print(f"[OCR Wait] Job {job_id} {job.status.value} after {elapsed:.1f}s ({iteration} iterations)", flush=True)
|
||||
return await get_job_status(job_id, session, current_user)
|
||||
|
||||
# Return if status changed from last check
|
||||
if last_status is not None and job.status != last_status:
|
||||
# Return on status change (unless wait_for_terminal is set)
|
||||
if not wait_for_terminal and last_status is not None and job.status != last_status:
|
||||
elapsed = time.time() - start_time
|
||||
print(f"[OCR Wait] Job {job_id} status changed {last_status.value}->{job.status.value} after {elapsed:.1f}s", flush=True)
|
||||
return await get_job_status(job_id, session, current_user)
|
||||
|
||||
last_status = job.status
|
||||
|
||||
# Wait 1 second before next internal check
|
||||
await asyncio.sleep(1)
|
||||
# Wait 500ms before next internal check (faster polling for better responsiveness)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Timeout - return current status
|
||||
elapsed = time.time() - start_time
|
||||
print(f"[OCR Wait] Job {job_id} timeout after {elapsed:.1f}s ({iteration} iterations), status={last_status.value if last_status else 'unknown'}", flush=True)
|
||||
return await get_job_status(job_id, session, current_user)
|
||||
|
||||
|
||||
@@ -417,7 +438,7 @@ async def _apply_fuzzy_cui_matching(
|
||||
if match:
|
||||
corrected_cui, supplier_name = match
|
||||
if corrected_cui != extraction_data.cui:
|
||||
print(f"[Fuzzy Match] Corrected: {extraction_data.cui} → {corrected_cui} ({supplier_name})", flush=True)
|
||||
print(f"[Fuzzy Match] Corrected: {extraction_data.cui} -> {corrected_cui} ({supplier_name})", flush=True)
|
||||
extraction_data.cui = corrected_cui
|
||||
# Also set partner_name if not already set
|
||||
if not extraction_data.partner_name:
|
||||
|
||||
Reference in New Issue
Block a user