All checks were successful
Deploy / deploy (push) Successful in 30s
- Full FastAPI sync engine: master→replica document sync via paperless REST API - Web UI: dashboard, replicas, logs, settings (Jinja2 + HTMX + Pico CSS) - APScheduler background sync, SSE live log stream, Prometheus metrics - Fernet encryption for API tokens at rest - pngx.env credential file: written on save, pre-fills forms on load - Dockerfile with layer-cached uv build, Python healthcheck - docker-compose with host networking for Tailscale access - Gitea Actions workflow: version bump, secret injection, docker compose deploy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""Dashboard status endpoint."""
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlmodel import Session, select
|
|
|
|
from ..database import get_session
|
|
from ..models import Replica, SyncRun
|
|
from ..sync.engine import get_progress
|
|
|
|
router = APIRouter(prefix="/api", tags=["status"])
|
|
|
|
|
|
@router.get("/status")
|
|
def get_status(session: Session = Depends(get_session)):
|
|
replicas = session.exec(select(Replica)).all()
|
|
progress = get_progress()
|
|
now = datetime.now(timezone.utc)
|
|
|
|
replica_data = []
|
|
for r in replicas:
|
|
lag = None
|
|
if r.last_sync_ts:
|
|
ts = r.last_sync_ts
|
|
if ts.tzinfo is None:
|
|
ts = ts.replace(tzinfo=timezone.utc)
|
|
lag = int((now - ts).total_seconds())
|
|
|
|
if r.suspended_at:
|
|
status = "suspended"
|
|
elif progress.running and progress.phase and r.name in progress.phase:
|
|
status = "syncing"
|
|
elif r.consecutive_failures > 0:
|
|
status = "error"
|
|
elif r.last_sync_ts:
|
|
status = "synced"
|
|
else:
|
|
status = "pending"
|
|
|
|
# Last run stats for this replica
|
|
last_run = session.exec(
|
|
select(SyncRun)
|
|
.where(SyncRun.replica_id == r.id)
|
|
.order_by(SyncRun.started_at.desc()) # type: ignore[attr-defined]
|
|
.limit(1)
|
|
).first()
|
|
|
|
replica_data.append(
|
|
{
|
|
"id": r.id,
|
|
"name": r.name,
|
|
"url": r.url,
|
|
"enabled": r.enabled,
|
|
"status": status,
|
|
"lag_seconds": lag,
|
|
"last_sync_ts": r.last_sync_ts.isoformat() if r.last_sync_ts else None,
|
|
"consecutive_failures": r.consecutive_failures,
|
|
"suspended": r.suspended_at is not None,
|
|
"docs_synced_last_run": last_run.docs_synced if last_run else 0,
|
|
"docs_failed_last_run": last_run.docs_failed if last_run else 0,
|
|
}
|
|
)
|
|
|
|
last_run = session.exec(
|
|
select(SyncRun)
|
|
.order_by(SyncRun.started_at.desc()) # type: ignore[attr-defined]
|
|
.limit(1)
|
|
).first()
|
|
|
|
return {
|
|
"replicas": replica_data,
|
|
"sync_progress": {
|
|
"running": progress.running,
|
|
"phase": progress.phase,
|
|
"docs_done": progress.docs_done,
|
|
"docs_total": progress.docs_total,
|
|
},
|
|
"last_sync_run": {
|
|
"id": last_run.id,
|
|
"started_at": last_run.started_at.isoformat() if last_run and last_run.started_at else None,
|
|
"finished_at": last_run.finished_at.isoformat() if last_run and last_run.finished_at else None,
|
|
"docs_synced": last_run.docs_synced if last_run else 0,
|
|
"docs_failed": last_run.docs_failed if last_run else 0,
|
|
"timed_out": last_run.timed_out if last_run else False,
|
|
}
|
|
if last_run
|
|
else None,
|
|
}
|